smart_phone_parser 1.0.4
smart_phone_parser: ^1.0.4 copied to clipboard
A zero-dependency global phone parser and selector supporting multiple UI design styles with automatic input normalization.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:smart_phone_parser/smart_phone_parser.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Smart Phone Parser Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Multiple controllers for different examples
final simpleController = SmartPhoneController();
final profileController = SmartPhoneController();
final favoritesController = SmartPhoneController();
final formController = SmartPhoneController();
final dialogController = SmartPhoneController();
final customStyleController = SmartPhoneController();
final minimalController = SmartPhoneController();
final maskController = SmartPhoneController();
final _formKey = GlobalKey<FormState>();
String _submittedNumber = '';
@override
void initState() {
super.initState();
// Load default number for profile example
profileController.setFullNumber('+201234567890');
}
@override
void dispose() {
simpleController.dispose();
profileController.dispose();
favoritesController.dispose();
formController.dispose();
dialogController.dispose();
customStyleController.dispose();
minimalController.dispose();
maskController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 5,
child: Scaffold( //this work with your local lang if you need to test wrap with Directionality!!
appBar: AppBar(
title: const Text('Smart Phone Parser - All Examples'),
centerTitle: true,
bottom: const TabBar(
tabs: [
Tab(text: 'Basic'),
Tab(text: 'Features'),
Tab(text: 'Styles'),
Tab(text:'login'),
Tab(text: 'Format'),
],
),
),
body: TabBarView(
children: [
_buildBasicTab(),
_buildFeaturesTab(),
_buildStylesTab(),
LoginScreen(),
_buildFormatTab(),
],
),
),
);
}
// ==================== TAB 1: BASIC ====================
Widget _buildBasicTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildCard(
title: 'β¨ Minimal Example',
subtitle: 'Just 2 lines of code',
icon: Icons.code,
color: Colors.blue,
child: Column(
children: [
SmartPhoneField(
controller: simpleController,
hintText: 'Enter your phone number',
),
const SizedBox(height: 12),
ValueListenableBuilder<ParsedPhone>(
valueListenable: simpleController,
builder: (context, phone, _) {
return _buildInfoCard(phone);
},
),
],
),
),
const SizedBox(height: 16),
_buildCard(
title: 'π± Profile Edit',
subtitle: 'Load number from API automatically',
icon: Icons.person,
color: Colors.green,
child: Column(
children: [
SmartPhoneField(
controller: profileController,
labelText: 'Phone Number',
hintText: 'Enter your phone number',
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () =>
profileController.setFullNumber('+201234567890'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: const Text('πͺπ¬ Egypt'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () =>
profileController.setFullNumber('+966512345678'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('πΈπ¦ Saudi'),
),
),
],
),
const SizedBox(height: 12),
ValueListenableBuilder<ParsedPhone>(
valueListenable: profileController,
builder: (context, phone, _) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${phone.flagEmoji} ${phone.country.name}'),
Text(phone.dialCode),
Text(
phone.nationalNumber.isEmpty
? "..."
: phone.nationalNumber,
),
],
),
);
},
),
],
),
),
const SizedBox(height: 16),
_buildCard(
title: 'π·οΈ With Labels',
subtitle: 'Label and Helper Text',
icon: Icons.label,
color: Colors.purple,
child: SmartPhoneField(
controller: SmartPhoneController(),
labelText: 'Mobile Number',
hintText: 'Enter your mobile number',
helperText: 'We will send verification code to this number',
prefixIcon: const Icon(Icons.phone),
),
),
],
),
);
}
// ==================== TAB 2: FEATURES ====================
Widget _buildFeaturesTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildCard(
title: 'β Favorite Countries',
subtitle: 'Pin frequently used countries at the top',
icon: Icons.star,
color: Colors.amber,
child: Column(
children: [
SmartPhoneField(
controller: favoritesController,
labelText: 'Phone Number',
favoriteCountries: const ['EG', 'SA', 'AE', 'US', 'GB'],
hintText: 'Enter your number',
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(Icons.info, size: 16),
SizedBox(width: 8),
Expanded(
child: Text(
'β Favorite countries appear at the top of the country picker',
style: TextStyle(fontSize: 12),
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
_buildCard(
title: 'β
Form Validation',
subtitle: 'With Form widget validation',
icon: Icons.check_circle,
color: Colors.green,
child: Form(
key: _formKey,
child: Column(
children: [
SmartPhoneField(
controller: formController,
labelText: 'Phone Number *',
hintText: 'Enter a valid international number',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Phone number is required';
}
if (!formController.isValid) {
return 'Please enter a valid phone number';
}
return null;
},
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
setState(() {
_submittedNumber = formController.fullNumber;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('β
Valid number!'),
backgroundColor: Colors.green,
),
);
}
},
icon: const Icon(Icons.check),
label: const Text('Submit'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 45),
),
),
if (_submittedNumber.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text('β
Submitted: $_submittedNumber'),
),
),
],
),
),
),
const SizedBox(height: 16),
_buildCard(
title: 'π± Dialog Picker',
subtitle: 'Alternative to Bottom Sheet',
icon: Icons.arrow_drop_up,
color: Colors.purple,
child: Column(
children: [
SmartPhoneField(
controller: dialogController,
labelText: 'Phone Number',
pickerType: CountryPickerType.dialog,
hintText: 'Tap country to change',
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(Icons.info, size: 16),
SizedBox(width: 8),
Expanded(
child: Text(
'π± Great for tablets and web platforms',
style: TextStyle(fontSize: 12),
),
),
],
),
),
],
),
),
],
),
);
}
// ==================== TAB 3: STYLES ====================
Widget _buildStylesTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildCard(
title: 'π¨ Custom Styling',
subtitle: 'Custom colors and borders',
icon: Icons.palette,
color: Colors.deepPurple,
child: SmartPhoneField(
controller: customStyleController,
labelText: 'Custom Design',
hintText: 'Enter your number',
theme: const SmartPhoneTheme(
borderRadius: BorderRadius.all(Radius.circular(30)),
borderColor: Colors.deepPurple,
focusedBorderColor: Colors.deepPurple,
backgroundColor: Color(0xFFEDE7F6),
flagSize: 28,
),
),
),
const SizedBox(height: 16),
_buildCard(
title: 'π― Minimal Style',
subtitle: 'Clean, no borders',
icon: Icons.minimize,
color: Colors.grey,
child: SmartPhoneField(
controller: minimalController,
hintText: 'Phone number',
theme: const SmartPhoneTheme(
backgroundColor: Colors.transparent,
borderColor: Colors.transparent,
focusedBorderColor: Colors.transparent,
),
),
),
const SizedBox(height: 16),
_buildCard(
title: 'π§ Custom Builder',
subtitle: '100% UI control',
icon: Icons.build,
color: Colors.orange,
child: SmartPhoneField.custom(
controller: customStyleController,
builder: (context, controller, phone, openPicker) {
return Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFF9800), Color(0xFFE65100)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
GestureDetector(
onTap: openPicker,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Row(
children: [
Text(
phone.flagEmoji,
style: const TextStyle(fontSize: 28),
),
const SizedBox(width: 8),
Text(
phone.dialCode,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
Container(
width: 1,
height: 30,
color: Colors.white.withValues(alpha:0.3),
),
Expanded(
child: TextField(
controller: controller.textController,
keyboardType: TextInputType.phone,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
decoration: const InputDecoration(
hintText: 'Enter number',
hintStyle: TextStyle(color: Colors.white70),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
),
),
),
),
],
),
);
},
),
),
],
),
);
}
// ==================== TAB 4: FORMAT ====================
Widget _buildFormatTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildCard(
title: 'π¨ Format Examples',
subtitle: 'Different formatting options',
icon: Icons.format_align_left,
color: Colors.indigo,
child: Column(
children: [
SmartPhoneField(
controller: maskController,
labelText: 'Enter phone number',
hintText: '+201234567890',
),
const SizedBox(height: 16),
ValueListenableBuilder<ParsedPhone>(
valueListenable: maskController,
builder: (context, phone, _) {
if (!phone.isValid) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'π Enter a valid number to see formatting options',
textAlign: TextAlign.center,
),
);
}
return Column(
children: [
_buildFormatRow('E.164', PhoneFormatter.toE164(phone)),
_buildFormatRow(
'Readable',
PhoneFormatter.toReadable(phone),
),
_buildFormatRow('Local', PhoneFormatter.toLocal(phone)),
_buildFormatRow(
'Mask ### #### ####',
PhoneFormatter.withMask(phone, mask: '### #### ####'),
),
_buildFormatRow(
'Mask ##-###-####',
PhoneFormatter.withMask(phone, mask: '##-###-####'),
),
_buildFormatRow(
'Mask (###) ###-####',
PhoneFormatter.withMask(
phone,
mask: '(###) ###-####',
),
),
_buildFormatRow(
'Country Format',
PhoneFormatter.withCountryFormat(phone),
),
],
);
},
),
],
),
),
],
),
);
}
// ==================== HELPER WIDGETS ====================
Widget _buildCard({
required String title,
required String subtitle,
required IconData icon,
required Color color,
required Widget child,
}) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha:0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
const SizedBox(height: 16),
child,
],
),
),
);
}
Widget _buildInfoCard(ParsedPhone phone) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text('π Country: ${phone.country.phoneCode} ${phone.flagEmoji}'),
const SizedBox(height: 4),
Text(
'π Full Number: ${phone.fullNumber.isEmpty ? "..." : phone.fullNumber}',
),
const SizedBox(height: 4),
Text('β
Valid: ${phone.isValid ? "Yes β
" : "No β"}'),
],
),
);
}
Widget _buildFormatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 150,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(6),
),
child: SelectableText(
value,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
),
],
),
);
}
}
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final SmartPhoneController _phoneController = SmartPhoneController();
final TextEditingController _passwordController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final String _validationMessage = '';
void _login() {
if (_formKey.currentState!.validate()) {
final phone = _phoneController.fullNumber;
final password = _passwordController.text;
log('Phone: $phone');
log('Password: $password');
log('Country: ${_phoneController.countryName}');
log('Valid: ${_phoneController.isValid}');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SmartPhoneField(
controller: _phoneController,
labelText: 'Phone Number',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Phone required';
}
final rules = PhoneRules.getRules(_phoneController.countryCode);
if (rules != null) {
final isValid = PhoneRules.isValidNumber(
_phoneController.countryCode,
_phoneController.nationalNumber,
);
if (!isValid) {
return PhoneRules.getErrorMessage(
_phoneController.countryCode,
_phoneController.nationalNumber,
);
}
}
// if (!_phoneController.isValid) {
// return 'Invalid phone number';
// }
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
validator: (v) => v == null || v.isEmpty ? 'Password required' : null,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _login,
child: const Text('Login'),
),
const SizedBox(height: 16),
if (_validationMessage.isNotEmpty)
Text(
_validationMessage,
style: const TextStyle(color: Colors.red),
),
],
),
),
),
);
}
}