Enhance Flutter app with animations, contacts access, image picker, and permissions handler
This commit is contained in:
parent
f5a2e02839
commit
6f11b367b6
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
|
||||
final Widget child;
|
||||
|
||||
FadeSlidePageRoute({required this.child})
|
||||
: super(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final fadeAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
|
||||
final slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.05),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: slideAnimation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:ui';
|
||||
import '../theme.dart';
|
||||
import '../route_transitions.dart';
|
||||
import 'customize_setup_screen.dart';
|
||||
|
||||
class BusinessSetupScreen extends StatefulWidget {
|
||||
|
|
@ -44,6 +48,105 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
{'id': 'others', 'name': 'Others', 'icon': Icons.grid_view},
|
||||
];
|
||||
|
||||
Future<void> _handleImagePick(String docName, ImageSource source) async {
|
||||
final permission = source == ImageSource.camera ? Permission.camera : Permission.photos;
|
||||
var status = await permission.status;
|
||||
if (!mounted) return;
|
||||
|
||||
if (status.isDenied && !kIsWeb) {
|
||||
final String sourceName = source == ImageSource.camera ? 'Camera' : 'Photos/Gallery';
|
||||
final bool? proceed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('$sourceName Permission Required'),
|
||||
content: Text(
|
||||
'PozoApp requires $sourceName permission to upload document verification files and store facade images.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Continue'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (proceed != true) return;
|
||||
status = await permission.request();
|
||||
if (!mounted) return;
|
||||
}
|
||||
|
||||
if (status.isGranted || kIsWeb) {
|
||||
try {
|
||||
final picker = ImagePicker();
|
||||
final XFile? file = await picker.pickImage(source: source);
|
||||
if (!mounted) return;
|
||||
if (file != null) {
|
||||
setState(() {
|
||||
_uploadedDocs[docName] = file.name;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('✅ Successfully loaded $docName!'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to pick image: $e'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Permission denied. Cannot load document image.'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showFacadePhotoOptions() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)),
|
||||
title: const Text('Take Photo'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_handleImagePick('facade_photo', ImageSource.camera);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)),
|
||||
title: const Text('Choose from Gallery'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_handleImagePick('facade_photo', ImageSource.gallery);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocChip(String label, IconData icon) {
|
||||
final theme = Theme.of(context);
|
||||
final isSelected = _selectedDocs.contains(label);
|
||||
|
|
@ -180,7 +283,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Size: 1.2 MB • Ready',
|
||||
'Attached • Ready',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
|
|
@ -240,18 +343,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_uploadedDocs[docName] = '${docName.toLowerCase().replaceAll(' ', '_')}_scan.jpg';
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('📸 Captured photo for $docName!'),
|
||||
backgroundColor: const Color(0xFFAF101A),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: () => _handleImagePick(docName, ImageSource.camera),
|
||||
icon: const Icon(Icons.camera_alt, size: 14),
|
||||
label: const Text('Take Photo'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
|
@ -261,24 +353,12 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
elevation: 1,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_uploadedDocs[docName] = 'gallery_${docName.toLowerCase().replaceAll(' ', '_')}.png';
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('📂 Uploaded $docName from gallery!'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: () => _handleImagePick(docName, ImageSource.gallery),
|
||||
icon: const Icon(Icons.photo_library, size: 14),
|
||||
label: const Text('From Gallery'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
|
|
@ -286,8 +366,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
side: const BorderSide(color: Color(0xFFAF101A)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -339,7 +418,8 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
child: Row(
|
||||
children: List.generate(5, (index) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -379,7 +459,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Name Field Input
|
||||
Text(
|
||||
'Business/Shop Name *',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
|
|
@ -397,7 +476,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category selection Block
|
||||
Text(
|
||||
'Business Type *',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
|
|
@ -407,7 +485,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Category custom grid (2x3)
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
|
|
@ -478,7 +555,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// GSTIN optional Input group with Verify button on right-side
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -542,7 +618,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Shop Location Detect
|
||||
Text(
|
||||
'Shop Location *',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
|
|
@ -619,7 +694,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Identity/License Documents
|
||||
Text(
|
||||
'Identity / License Documents (Select all that apply)',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
|
|
@ -640,7 +714,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Expanded Upload Sections
|
||||
..._selectedDocs.map((doc) {
|
||||
IconData docIcon = Icons.description;
|
||||
if (doc == 'GSTIN') docIcon = Icons.description;
|
||||
|
|
@ -651,7 +724,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
return _buildUploadSection(doc, docIcon);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
// Shop Facade Photo upload area
|
||||
|
||||
Text(
|
||||
'Shop Facade Photo *',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
|
|
@ -662,12 +735,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('📸 Camera / Photo Upload Clicked!'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
_showFacadePhotoOptions();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
|
|
@ -676,7 +744,8 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outlineVariant,
|
||||
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.outlineVariant,
|
||||
width: _uploadedDocs.containsKey('facade_photo') ? 2.0 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
|
|
@ -685,18 +754,18 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.1),
|
||||
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green.withOpacity(0.1) : theme.colorScheme.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add_a_photo_outlined,
|
||||
color: theme.colorScheme.primary,
|
||||
_uploadedDocs.containsKey('facade_photo') ? Icons.check_circle : Icons.add_a_photo_outlined,
|
||||
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Upload front view',
|
||||
_uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']! : 'Upload front view',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
|
|
@ -704,7 +773,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Clear photo showing shop board',
|
||||
_uploadedDocs.containsKey('facade_photo') ? 'Facade Photo Attached • Tap to change' : 'Clear photo showing shop board',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
|
|
@ -721,7 +790,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
),
|
||||
),
|
||||
|
||||
// Button docked
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||
child: ElevatedButton(
|
||||
|
|
@ -729,8 +797,8 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|||
? () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CustomizeSetupScreen(
|
||||
FadeSlidePageRoute(
|
||||
child: CustomizeSetupScreen(
|
||||
businessName: _nameController.text.trim(),
|
||||
businessType: _selectedCategory,
|
||||
gstin: _gstinController.text.trim(),
|
||||
|
|
@ -805,7 +873,6 @@ class DashedBorderPainter extends CustomPainter {
|
|||
|
||||
canvas.drawPath(dashPath, paint);
|
||||
} catch (e) {
|
||||
// Fallback for platform/renderers (such as Flutter Web HTML renderer) that do not support computeMetrics()
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../theme.dart';
|
||||
import '../route_transitions.dart';
|
||||
import 'setup_complete_screen.dart';
|
||||
|
||||
class CustomizeSetupScreen extends StatefulWidget {
|
||||
|
|
@ -53,8 +54,8 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
|||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SetupCompleteScreen(
|
||||
FadeSlidePageRoute(
|
||||
child: SetupCompleteScreen(
|
||||
businessName: widget.businessName,
|
||||
businessType: widget.businessType,
|
||||
gstin: widget.gstin,
|
||||
|
|
@ -175,8 +176,8 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
|||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SetupCompleteScreen(
|
||||
FadeSlidePageRoute(
|
||||
child: SetupCompleteScreen(
|
||||
businessName: widget.businessName,
|
||||
businessType: widget.businessType,
|
||||
gstin: widget.gstin,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:contacts_service/contacts_service.dart';
|
||||
import '../theme.dart';
|
||||
import '../route_transitions.dart';
|
||||
import 'otp_verification_screen.dart';
|
||||
|
||||
class MockContact {
|
||||
final String? displayName;
|
||||
final List<MockPhone> phones;
|
||||
|
||||
MockContact({this.displayName, required String phoneNumber})
|
||||
: phones = [MockPhone(phoneNumber)];
|
||||
}
|
||||
|
||||
class MockPhone {
|
||||
final String? value;
|
||||
MockPhone(this.value);
|
||||
}
|
||||
|
||||
class MobileLoginScreen extends StatefulWidget {
|
||||
const MobileLoginScreen({super.key});
|
||||
|
||||
|
|
@ -32,6 +49,176 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickContact() async {
|
||||
const permission = Permission.contacts;
|
||||
var status = await permission.status;
|
||||
if (!mounted) return;
|
||||
|
||||
if (status.isDenied) {
|
||||
final bool? proceed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Contacts Permission Required'),
|
||||
content: const Text(
|
||||
'PozoApp requires access to your contacts to quickly auto-fill mobile numbers during registration/billing.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Continue'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (proceed != true) return;
|
||||
status = await permission.request();
|
||||
if (!mounted) return;
|
||||
}
|
||||
|
||||
if (status.isGranted || kIsWeb) {
|
||||
_showContactPicker();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Contacts permission was denied. Please enter the number manually or enable it in Settings.'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> _loadContacts() async {
|
||||
if (kIsWeb) {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
return [
|
||||
MockContact(displayName: 'Amit Sharma', phoneNumber: '9876543210'),
|
||||
MockContact(displayName: 'Rajesh Patel', phoneNumber: '9823456789'),
|
||||
MockContact(displayName: 'Priya Sundaram', phoneNumber: '8765432109'),
|
||||
MockContact(displayName: 'Karan Malhotra', phoneNumber: '7654321098'),
|
||||
];
|
||||
} else {
|
||||
try {
|
||||
final List<Contact> list = await ContactsService.getContacts(withThumbnails: false);
|
||||
return list;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _selectContactNumber(String? rawNumber) {
|
||||
if (rawNumber == null) return;
|
||||
var digits = rawNumber.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length == 12 && digits.startsWith('91')) {
|
||||
digits = digits.substring(2);
|
||||
}
|
||||
if (digits.length == 11 && digits.startsWith('0')) {
|
||||
digits = digits.substring(1);
|
||||
}
|
||||
if (digits.length > 10) {
|
||||
digits = digits.substring(digits.length - 10);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_controller.text = digits;
|
||||
_isValid = digits.length == 10;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showContactPicker() async {
|
||||
List<dynamic> allContacts = [];
|
||||
List<dynamic> filteredContacts = [];
|
||||
bool isLoading = true;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
if (isLoading) {
|
||||
_loadContacts().then((loaded) {
|
||||
setDialogState(() {
|
||||
allContacts = loaded;
|
||||
filteredContacts = loaded;
|
||||
isLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Select Contact'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search contacts...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
onChanged: (query) {
|
||||
setDialogState(() {
|
||||
filteredContacts = allContacts.where((contact) {
|
||||
final name = (contact.displayName ?? '').toLowerCase();
|
||||
final phone = (contact.phones.isNotEmpty ? contact.phones.first.value ?? '' : '').toLowerCase();
|
||||
return name.contains(query.toLowerCase()) || phone.contains(query.toLowerCase());
|
||||
}).toList();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: filteredContacts.isEmpty
|
||||
? const Center(child: Text('No contacts found'))
|
||||
: ListView.builder(
|
||||
itemCount: filteredContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = filteredContacts[index];
|
||||
final name = contact.displayName ?? 'Unknown';
|
||||
final phone = contact.phones.isNotEmpty ? contact.phones.first.value : 'No number';
|
||||
return ListTile(
|
||||
title: Text(name),
|
||||
subtitle: Text(phone),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: const Color(0xFFAF101A).withOpacity(0.1),
|
||||
child: Text(
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||
style: const TextStyle(color: Color(0xFFAF101A), fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
_selectContactNumber(phone);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -70,7 +257,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Progress Bar: Step 2 of 5 with "Mobile Login" label on right
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -112,7 +298,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Title Header & Secure Badge Group
|
||||
Text(
|
||||
'Welcome Back',
|
||||
style: theme.textTheme.headlineLarge?.copyWith(
|
||||
|
|
@ -126,7 +311,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 6. Top Badge: Green "Secure OTP Verification" chip with lock icon
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -152,11 +336,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. Truecaller Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3F3F3), // light gray background (surface low)
|
||||
color: const Color(0xFFF3F3F3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
|
||||
),
|
||||
|
|
@ -172,7 +355,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.verified_user, // blue shield icon
|
||||
Icons.verified_user,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
|
|
@ -228,8 +411,8 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const OtpVerificationScreen(
|
||||
FadeSlidePageRoute(
|
||||
child: const OtpVerificationScreen(
|
||||
mobileNumber: '9876543210',
|
||||
),
|
||||
),
|
||||
|
|
@ -249,7 +432,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(56),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 8px for buttons
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 1,
|
||||
),
|
||||
|
|
@ -269,7 +452,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
|
||||
SizedBox(height: gap),
|
||||
|
||||
// 3. Divider: "Or enter mobile number manually"
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
||||
|
|
@ -299,7 +481,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 4. Mobile Number field: Country code group on left, number field on right
|
||||
Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -316,7 +497,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Flag and code prefix box
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -326,7 +506,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Indian flag representation
|
||||
Container(
|
||||
width: 22,
|
||||
height: 14,
|
||||
|
|
@ -366,7 +545,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
),
|
||||
|
||||
// Input field
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
|
|
@ -388,7 +566,11 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.contact_phone, color: Color(0xFFAF101A)),
|
||||
onPressed: _pickContact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -398,18 +580,16 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
),
|
||||
),
|
||||
|
||||
// 5. Bottom buttons
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Full width red "Send OTP" button
|
||||
ElevatedButton(
|
||||
onPressed: _isValid
|
||||
? () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => OtpVerificationScreen(
|
||||
FadeSlidePageRoute(
|
||||
child: OtpVerificationScreen(
|
||||
mobileNumber: _controller.text,
|
||||
),
|
||||
),
|
||||
|
|
@ -422,13 +602,12 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
foregroundColor: _isValid ? Colors.white : Colors.grey[400],
|
||||
elevation: _isValid ? 2 : 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 8px default button radius
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Send OTP'),
|
||||
),
|
||||
SizedBox(height: gap / 2),
|
||||
// Outline style red "Help / Talk to Support" button with chat icon
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -442,7 +621,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
|||
minimumSize: const Size.fromHeight(56),
|
||||
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 8px default button radius
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme.dart';
|
||||
import '../route_transitions.dart';
|
||||
import 'business_setup_screen.dart';
|
||||
|
||||
class OtpVerificationScreen extends StatefulWidget {
|
||||
|
|
@ -331,8 +332,8 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
|||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BusinessSetupScreen(),
|
||||
FadeSlidePageRoute(
|
||||
child: const BusinessSetupScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../theme.dart';
|
||||
import '../route_transitions.dart';
|
||||
import 'billing_dashboard_screen.dart';
|
||||
|
||||
class SetupCompleteScreen extends StatelessWidget {
|
||||
|
|
@ -302,8 +303,8 @@ class SetupCompleteScreen extends StatelessWidget {
|
|||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BillingDashboardScreen(
|
||||
FadeSlidePageRoute(
|
||||
child: BillingDashboardScreen(
|
||||
businessName: businessName,
|
||||
businessType: businessType,
|
||||
gstin: gstin,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,67 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../theme.dart';
|
||||
import '../route_transitions.dart';
|
||||
import 'mobile_login_screen.dart';
|
||||
|
||||
class WelcomeScreen extends StatelessWidget {
|
||||
class WelcomeScreen extends StatefulWidget {
|
||||
const WelcomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||
}
|
||||
|
||||
class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<double> _logoScaleAnimation;
|
||||
late Animation<double> _logoRotateAnimation;
|
||||
double _btnScale = 1.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
);
|
||||
|
||||
_fadeAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
|
||||
_logoScaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.2, 0.8, curve: Curves.elasticOut),
|
||||
),
|
||||
);
|
||||
|
||||
_logoRotateAnimation = Tween<double>(begin: -0.1, end: 0.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.2, 0.8, curve: Curves.easeOutBack),
|
||||
),
|
||||
);
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isMobile = context.isMobile;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
body: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Progress bar indicator - 1st segment active
|
||||
|
|
@ -20,7 +70,8 @@ class WelcomeScreen extends StatelessWidget {
|
|||
child: Row(
|
||||
children: List.generate(5, (index) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -142,11 +193,16 @@ class WelcomeScreen extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogo(ThemeData theme) {
|
||||
return Container(
|
||||
return ScaleTransition(
|
||||
scale: _logoScaleAnimation,
|
||||
child: RotationTransition(
|
||||
turns: _logoRotateAnimation,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -194,6 +250,8 @@ class WelcomeScreen extends StatelessWidget {
|
|||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -385,11 +443,22 @@ class WelcomeScreen extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: context.isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => _btnScale = 1.03),
|
||||
onExit: (_) => setState(() => _btnScale = 1.0),
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _btnScale = 0.95),
|
||||
onTapUp: (_) => setState(() => _btnScale = 1.03),
|
||||
onTapCancel: () => setState(() => _btnScale = 1.0),
|
||||
child: AnimatedScale(
|
||||
scale: _btnScale,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeOut,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MobileLoginScreen()),
|
||||
FadeSlidePageRoute(child: const MobileLoginScreen()),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
|
@ -405,6 +474,9 @@ class WelcomeScreen extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: context.isMobile ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
contacts_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: contacts_service
|
||||
sha256: f6d5ea33b31dfcdcd2e65d8abdc836502e04ddb0f66a96aa726fa9891ea9671e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +89,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+4"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -86,11 +134,24 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.31"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -115,6 +176,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -171,6 +296,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -227,6 +360,54 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: e20daf680eef1ca62ffe8c8c526b778cc386d50137c77ac71c8ec9c88c13fb9d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.9"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -243,6 +424,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -337,5 +526,5 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ dependencies:
|
|||
sdk: flutter
|
||||
cupertino_icons: ^1.0.5
|
||||
google_fonts: ^5.1.0
|
||||
image_picker: ^1.0.4
|
||||
permission_handler: ^11.0.1
|
||||
contacts_service: ^0.6.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue