diff --git a/flutter_app/assets/sounds/click.mp3 b/flutter_app/assets/sounds/click.mp3 new file mode 100644 index 0000000..44b2df6 Binary files /dev/null and b/flutter_app/assets/sounds/click.mp3 differ diff --git a/flutter_app/assets/sounds/error.mp3 b/flutter_app/assets/sounds/error.mp3 new file mode 100644 index 0000000..3fe0de5 Binary files /dev/null and b/flutter_app/assets/sounds/error.mp3 differ diff --git a/flutter_app/assets/sounds/select.mp3 b/flutter_app/assets/sounds/select.mp3 new file mode 100644 index 0000000..88c535b Binary files /dev/null and b/flutter_app/assets/sounds/select.mp3 differ diff --git a/flutter_app/assets/sounds/success.mp3 b/flutter_app/assets/sounds/success.mp3 new file mode 100644 index 0000000..629e4b6 Binary files /dev/null and b/flutter_app/assets/sounds/success.mp3 differ diff --git a/flutter_app/ios/Runner/Info.plist b/flutter_app/ios/Runner/Info.plist index 0b8b011..8c9cbf0 100644 --- a/flutter_app/ios/Runner/Info.plist +++ b/flutter_app/ios/Runner/Info.plist @@ -51,5 +51,13 @@ PozoApp requires photo gallery access to upload documents and shop facade images. NSContactsUsageDescription PozoApp requires contacts access to let you search and auto-fill mobile numbers during registration and billing. + NSLocationWhenInUseUsageDescription + PozoApp needs your location to automatically fetch and verify your shop's address. + NSLocationAlwaysUsageDescription + PozoApp needs your location to automatically fetch and verify your shop's address. + LSApplicationQueriesSchemes + + tel + diff --git a/flutter_app/lib/feedback_helper.dart b/flutter_app/lib/feedback_helper.dart new file mode 100644 index 0000000..a70a6ac --- /dev/null +++ b/flutter_app/lib/feedback_helper.dart @@ -0,0 +1,79 @@ +import 'package:flutter/services.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:vibration/vibration.dart'; +import 'package:flutter/foundation.dart' show kIsWeb, debugPrint; + +class AppFeedback { + static final AudioPlayer _player = AudioPlayer(); + + /// Play a sound from assets/sounds/ + static Future playSound(String fileName) async { + try { + // AudioPlayer play from assets/sounds/fileName + await _player.play(AssetSource('sounds/$fileName')); + } catch (e) { + // Web might throw security exception if player triggers before user interaction + // or other runtime issues. Catching prevents app crashes. + debugPrint('Audio playback error for $fileName: $e'); + } + } + + /// Light haptic feedback + subtle click sound + static void click() { + HapticFeedback.lightImpact(); + playSound('click.mp3'); + } + + /// Medium haptic + selection sound + static void select() { + HapticFeedback.mediumImpact(); + playSound('select.mp3'); + } + + /// Celebration sound + strong haptic vibration + static void success() { + HapticFeedback.heavyImpact(); + playSound('success.mp3'); + } + + /// Wrong input -> error haptic (double vibrate) + error sound + static void error() { + _doubleVibrate(); + playSound('error.mp3'); + } + + /// Light haptic feedback (e.g. chip selections) + static void lightHaptic() { + HapticFeedback.lightImpact(); + } + + /// Medium haptic feedback + static void mediumHaptic() { + HapticFeedback.mediumImpact(); + } + + /// Heavy haptic feedback + static void heavyHaptic() { + HapticFeedback.heavyImpact(); + } + + /// Subtle haptic feedback (e.g. screen navigation) + static void subtleHaptic() { + HapticFeedback.selectionClick(); + } + + /// Double vibrate pattern using vibration package + static Future _doubleVibrate() async { + try { + if (!kIsWeb && await Vibration.hasVibrator() == true) { + // Double vibrate pattern: wait 0, vibrate 100ms, wait 100ms, vibrate 100ms + Vibration.vibrate(pattern: [0, 100, 100, 100]); + } else { + // Fallback to native HapticFeedback + HapticFeedback.mediumImpact(); + } + } catch (e) { + HapticFeedback.mediumImpact(); + } + } +} diff --git a/flutter_app/lib/route_transitions.dart b/flutter_app/lib/route_transitions.dart index 6a59a28..8056224 100644 --- a/flutter_app/lib/route_transitions.dart +++ b/flutter_app/lib/route_transitions.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'feedback_helper.dart'; class FadeSlidePageRoute extends PageRouteBuilder { final Widget child; @@ -33,7 +34,9 @@ class FadeSlidePageRoute extends PageRouteBuilder { }, transitionDuration: const Duration(milliseconds: 300), reverseTransitionDuration: const Duration(milliseconds: 200), - ); + ) { + AppFeedback.subtleHaptic(); + } } void showTopAlert( diff --git a/flutter_app/lib/screens/billing_dashboard_screen.dart b/flutter_app/lib/screens/billing_dashboard_screen.dart index 3207a67..b4ca602 100644 --- a/flutter_app/lib/screens/billing_dashboard_screen.dart +++ b/flutter_app/lib/screens/billing_dashboard_screen.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; class ProductItem { final String id; @@ -136,6 +137,7 @@ class _BillingDashboardScreenState extends State { } void _simulateBarcodeScan() { + AppFeedback.click(); if (!widget.barcodeScanner) return; final random = Random(); final randomProduct = _products[random.nextInt(_products.length)]; @@ -177,6 +179,7 @@ class _BillingDashboardScreenState extends State { double get _total => _subtotal + _cgst + _sgst; void _handleReviewAndGenerate() { + AppFeedback.success(); final random = Random(); final invoiceVal = 'POZO-${random.nextInt(90000) + 10000}'; setState(() { @@ -186,6 +189,7 @@ class _BillingDashboardScreenState extends State { } void _closeReceiptAndReset() { + AppFeedback.click(); setState(() { _printingInvoice = false; _billingList.clear(); @@ -206,7 +210,10 @@ class _BillingDashboardScreenState extends State { elevation: 4, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -964,6 +971,7 @@ class _BillingDashboardScreenState extends State { InteractiveScale( child: ElevatedButton.icon( onPressed: () { + AppFeedback.click(); showTopAlert( context, 'Thermal Printed: $_invoiceNumber successfully!', diff --git a/flutter_app/lib/screens/business_setup_screen.dart b/flutter_app/lib/screens/business_setup_screen.dart index 6654fd3..c00a3d0 100644 --- a/flutter_app/lib/screens/business_setup_screen.dart +++ b/flutter_app/lib/screens/business_setup_screen.dart @@ -2,9 +2,13 @@ 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 'package:geolocator/geolocator.dart'; +import 'package:geocoding/geocoding.dart'; import 'dart:ui'; +import 'dart:io'; import '../theme.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; import 'customize_setup_screen.dart'; class BusinessSetupScreen extends StatefulWidget { @@ -17,10 +21,16 @@ class BusinessSetupScreen extends StatefulWidget { class _BusinessSetupScreenState extends State { final TextEditingController _nameController = TextEditingController(); final TextEditingController _gstinController = TextEditingController(); + final TextEditingController _addressController = TextEditingController(); + String _selectedCategory = 'supermarket'; bool _isNameValid = false; + + bool _isGstinValid = false; + bool _hasGstinInput = false; + final Set _selectedDocs = {'GSTIN', 'FSSAI'}; - final Map _uploadedDocs = {}; + final Map _uploadedDocs = {}; @override void initState() { @@ -30,12 +40,30 @@ class _BusinessSetupScreenState extends State { _isNameValid = _nameController.text.trim().isNotEmpty; }); }); + _gstinController.addListener(_validateGstin); + } + + void _validateGstin() { + final text = _gstinController.text.trim(); + if (text.isEmpty) { + setState(() { + _hasGstinInput = false; + _isGstinValid = false; + }); + return; + } + final RegExp gstinRegex = RegExp(r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[0-9]{1}Z[A-Z0-9]{1}$'); + setState(() { + _hasGstinInput = true; + _isGstinValid = gstinRegex.hasMatch(text); + }); } @override void dispose() { _nameController.dispose(); _gstinController.dispose(); + _addressController.dispose(); super.dispose(); } @@ -49,10 +77,16 @@ class _BusinessSetupScreenState extends State { ]; Future _handleImagePick(String docName, ImageSource source) async { + AppFeedback.click(); final permission = source == ImageSource.camera ? Permission.camera : Permission.photos; var status = await permission.status; if (!mounted) return; + if (status.isPermanentlyDenied) { + _showSettingsDialog(source == ImageSource.camera ? 'Camera' : 'Photos'); + return; + } + if (status.isDenied && !kIsWeb) { final String sourceName = source == ImageSource.camera ? 'Camera' : 'Photos/Gallery'; final bool? proceed = await showDialog( @@ -64,11 +98,17 @@ class _BusinessSetupScreenState extends State { ), actions: [ TextButton( - onPressed: () => Navigator.pop(context, false), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context, false); + }, child: const Text('Cancel'), ), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context, true); + }, child: const Text('Continue'), ), ], @@ -79,6 +119,11 @@ class _BusinessSetupScreenState extends State { if (proceed != true) return; status = await permission.request(); if (!mounted) return; + + if (status.isPermanentlyDenied) { + _showSettingsDialog(sourceName); + return; + } } if (status.isGranted || kIsWeb) { @@ -88,7 +133,7 @@ class _BusinessSetupScreenState extends State { if (!mounted) return; if (file != null) { setState(() { - _uploadedDocs[docName] = file.name; + _uploadedDocs[docName] = file; }); showTopAlert( context, @@ -113,6 +158,110 @@ class _BusinessSetupScreenState extends State { } } + void _showSettingsDialog(String permissionName) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('$permissionName Permission Disabled'), + content: Text('Please enable $permissionName access in system settings to proceed.'), + actions: [ + TextButton( + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + openAppSettings(); + }, + child: const Text('Open Settings'), + ), + ], + ), + ); + } + + Future _fetchLocationAndAddress() async { + AppFeedback.click(); + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + if (mounted) { + showTopAlert(context, 'Location services are disabled.', isError: true); + } + return; + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + if (mounted) { + showTopAlert(context, 'Location permissions are denied', isError: true); + } + return; + } + } + + if (permission == LocationPermission.deniedForever) { + if (mounted) { + _showSettingsDialog('Location'); + } + return; + } + + if (mounted) { + showTopAlert(context, 'Fetching location...', icon: Icons.gps_fixed); + } + + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + + if (kIsWeb) { + // Reverse geocoding might have CORS issues on web without a backend proxy or proper keys depending on provider, + // Mocking for web demonstration unless specifically supported + setState(() { + _addressController.text = 'Web Mock Address: Lat ${position.latitude.toStringAsFixed(4)}, Lng ${position.longitude.toStringAsFixed(4)}\nCity, State, Country'; + }); + if (mounted) { + showTopAlert(context, 'Location detected (Web Mock)', icon: Icons.my_location_outlined); + } + } else { + List placemarks = await placemarkFromCoordinates( + position.latitude, position.longitude); + + if (placemarks.isNotEmpty) { + Placemark place = placemarks[0]; + List parts = []; + if (place.name != null && place.name!.isNotEmpty) parts.add(place.name!); + if (place.subLocality != null && place.subLocality!.isNotEmpty) parts.add(place.subLocality!); + if (place.locality != null && place.locality!.isNotEmpty) parts.add(place.locality!); + if (place.administrativeArea != null && place.administrativeArea!.isNotEmpty) parts.add(place.administrativeArea!); + if (place.postalCode != null && place.postalCode!.isNotEmpty) parts.add(place.postalCode!); + + setState(() { + _addressController.text = parts.join(', '); + }); + + if (mounted) { + showTopAlert(context, 'Address auto-filled successfully!', icon: Icons.my_location_outlined); + } + } + } + } catch (e) { + if (mounted) { + showTopAlert(context, 'Failed to fetch address: $e', isError: true); + } + } + } + void _showFacadePhotoOptions() { showModalBottomSheet( context: context, @@ -124,6 +273,7 @@ class _BusinessSetupScreenState extends State { leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)), title: const Text('Take Photo'), onTap: () { + AppFeedback.click(); Navigator.pop(context); _handleImagePick('facade_photo', ImageSource.camera); }, @@ -132,6 +282,7 @@ class _BusinessSetupScreenState extends State { leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)), title: const Text('Choose from Gallery'), onTap: () { + AppFeedback.click(); Navigator.pop(context); _handleImagePick('facade_photo', ImageSource.gallery); }, @@ -149,13 +300,16 @@ class _BusinessSetupScreenState extends State { return InteractiveScale( child: InkWell( onTap: () { - setState(() { - if (isSelected) { + AppFeedback.lightHaptic(); + if (!isSelected) { + _selectedDocs.add(label); + _showFacadePhotoOptionsForDoc(label); + } else { + setState(() { _selectedDocs.remove(label); - } else { - _selectedDocs.add(label); - } - }); + _uploadedDocs.remove(label); + }); + } }, borderRadius: BorderRadius.circular(8), child: Container( @@ -193,6 +347,38 @@ class _BusinessSetupScreenState extends State { ); } + void _showFacadePhotoOptionsForDoc(String docName) { + 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: () { + AppFeedback.click(); + Navigator.pop(context); + _handleImagePick(docName, ImageSource.camera); + }, + ), + ListTile( + leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)), + title: const Text('Choose from Gallery'), + onTap: () { + AppFeedback.click(); + Navigator.pop(context); + _handleImagePick(docName, ImageSource.gallery); + }, + ), + ], + ), + ); + }, + ); + } + Widget _buildUploadSection(String docName, IconData icon) { final theme = Theme.of(context); final isUploaded = _uploadedDocs.containsKey(docName); @@ -231,6 +417,7 @@ class _BusinessSetupScreenState extends State { IconButton( icon: const Icon(Icons.close, size: 18, color: Colors.grey), onPressed: () { + AppFeedback.click(); setState(() { _selectedDocs.remove(docName); }); @@ -258,13 +445,20 @@ class _BusinessSetupScreenState extends State { color: Colors.red[50], borderRadius: BorderRadius.circular(8), ), - child: const Center( - child: Icon( - Icons.insert_drive_file, - color: Color(0xFFAF101A), - size: 24, - ), - ), + clipBehavior: Clip.antiAlias, + child: kIsWeb + ? Image.network( + _uploadedDocs[docName]!.path, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.broken_image, color: Color(0xFFAF101A)), + ) + : Image.file( + File(_uploadedDocs[docName]!.path), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.broken_image, color: Color(0xFFAF101A)), + ), ), const SizedBox(width: 12), Expanded( @@ -272,7 +466,7 @@ class _BusinessSetupScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _uploadedDocs[docName] ?? 'Attached_document.pdf', + _uploadedDocs[docName]?.name ?? 'Attached_document.jpg', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -293,6 +487,7 @@ class _BusinessSetupScreenState extends State { IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () { + AppFeedback.click(); setState(() { _uploadedDocs.remove(docName); }); @@ -392,7 +587,10 @@ class _BusinessSetupScreenState extends State { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)), - onPressed: () => Navigator.pop(context), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, ), title: Text( 'PozoApp', @@ -523,6 +721,7 @@ class _BusinessSetupScreenState extends State { return InteractiveScale( child: InkWell( onTap: () { + AppFeedback.select(); setState(() { _selectedCategory = cat['id']; }); @@ -612,8 +811,14 @@ class _BusinessSetupScreenState extends State { child: TextField( controller: _gstinController, textCapitalization: TextCapitalization.characters, - decoration: const InputDecoration( + decoration: InputDecoration( hintText: '15-DIGIT GST NUMBER', + suffixIcon: _hasGstinInput + ? Icon( + _isGstinValid ? Icons.check_circle : Icons.error_outline, + color: _isGstinValid ? Colors.green : Colors.red, + ) + : null, ), style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2), ), @@ -622,11 +827,21 @@ class _BusinessSetupScreenState extends State { InteractiveScale( child: ElevatedButton( onPressed: () { - showTopAlert( - context, - 'GSTIN Verified Successfully!', - icon: Icons.verified_outlined, - ); + if (_isGstinValid) { + AppFeedback.success(); + showTopAlert( + context, + 'GSTIN Verified Successfully!', + icon: Icons.verified_outlined, + ); + } else { + AppFeedback.error(); + showTopAlert( + context, + 'Invalid GSTIN Format. Check 15-digit code.', + isError: true, + ); + } }, style: ElevatedButton.styleFrom( minimumSize: const Size(0, 48), @@ -667,13 +882,7 @@ class _BusinessSetupScreenState extends State { const SizedBox(height: 8), InteractiveScale( child: InkWell( - onTap: () { - showTopAlert( - context, - 'Location detected: Sector 62, Noida, UP', - icon: Icons.my_location_outlined, - ); - }, + onTap: _fetchLocationAndAddress, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), @@ -731,6 +940,23 @@ class _BusinessSetupScreenState extends State { ), ), ), + const SizedBox(height: 16), + Text( + 'Shop Address *', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _addressController, + maxLines: 3, + decoration: const InputDecoration( + hintText: 'Enter complete shop address', + ), + style: const TextStyle(fontWeight: FontWeight.bold), + ), ], ), ), @@ -828,7 +1054,7 @@ class _BusinessSetupScreenState extends State { ), const SizedBox(height: 12), Text( - _uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']! : 'Upload front view', + _uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']!.name : 'Upload front view', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, @@ -867,6 +1093,7 @@ class _BusinessSetupScreenState extends State { child: ElevatedButton( onPressed: _isNameValid ? () { + AppFeedback.click(); Navigator.push( context, FadeSlidePageRoute( diff --git a/flutter_app/lib/screens/customize_setup_screen.dart b/flutter_app/lib/screens/customize_setup_screen.dart index 6692948..5b144fe 100644 --- a/flutter_app/lib/screens/customize_setup_screen.dart +++ b/flutter_app/lib/screens/customize_setup_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../theme.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; import 'setup_complete_screen.dart'; class CustomizeSetupScreen extends StatefulWidget { @@ -33,7 +34,10 @@ class _CustomizeSetupScreenState extends State { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)), - onPressed: () => Navigator.pop(context), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, ), title: Row( children: const [ @@ -52,6 +56,7 @@ class _CustomizeSetupScreenState extends State { actions: [ TextButton( onPressed: () { + AppFeedback.click(); Navigator.push( context, FadeSlidePageRoute( @@ -194,6 +199,7 @@ class _CustomizeSetupScreenState extends State { child: InteractiveScale( child: ElevatedButton( onPressed: () { + AppFeedback.click(); Navigator.push( context, FadeSlidePageRoute( @@ -235,8 +241,13 @@ class _CustomizeSetupScreenState extends State { ValueChanged onChanged, ThemeData theme, ) { + void wrappedOnChanged(bool val) { + AppFeedback.click(); + onChanged(val); + } + return InteractiveScale( - onTap: () => onChanged(!value), + onTap: () => wrappedOnChanged(!value), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -290,7 +301,7 @@ class _CustomizeSetupScreenState extends State { ), Switch( value: value, - onChanged: onChanged, + onChanged: wrappedOnChanged, activeColor: Colors.white, activeTrackColor: theme.colorScheme.primary, ), diff --git a/flutter_app/lib/screens/mobile_login_screen.dart b/flutter_app/lib/screens/mobile_login_screen.dart index a119abc..6263587 100644 --- a/flutter_app/lib/screens/mobile_login_screen.dart +++ b/flutter_app/lib/screens/mobile_login_screen.dart @@ -3,8 +3,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../theme.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; import 'otp_verification_screen.dart'; class MockContact { @@ -21,19 +23,23 @@ class MockPhone { } class MobileLoginScreen extends StatefulWidget { - const MobileLoginScreen({super.key}); + final String? autoFilledMobileNumber; + + const MobileLoginScreen({super.key, this.autoFilledMobileNumber}); @override State createState() => _MobileLoginScreenState(); } class _MobileLoginScreenState extends State { - final TextEditingController _controller = TextEditingController(); + late final TextEditingController _controller; bool _isValid = false; @override void initState() { super.initState(); + _controller = TextEditingController(text: widget.autoFilledMobileNumber ?? ''); + _isValid = _controller.text.length == 10; _controller.addListener(_validateInput); } @@ -50,6 +56,7 @@ class _MobileLoginScreenState extends State { } Future _pickContact() async { + AppFeedback.click(); const permission = Permission.contacts; var status = await permission.status; if (!mounted) return; @@ -195,6 +202,7 @@ class _MobileLoginScreenState extends State { ), ), onTap: () { + AppFeedback.click(); _selectContactNumber(phone); Navigator.pop(context); }, @@ -207,7 +215,10 @@ class _MobileLoginScreenState extends State { ), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, child: const Text('Close'), ) ], @@ -227,7 +238,10 @@ class _MobileLoginScreenState extends State { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)), - onPressed: () => Navigator.pop(context), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, ), title: Text( 'PozoApp', @@ -409,6 +423,7 @@ class _MobileLoginScreenState extends State { const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { + AppFeedback.click(); _controller.text = '9876543210'; setState(() { _isValid = true; @@ -605,6 +620,7 @@ class _MobileLoginScreenState extends State { child: ElevatedButton( onPressed: _isValid ? () { + AppFeedback.click(); Navigator.push( context, FadeSlidePageRoute( @@ -630,12 +646,28 @@ class _MobileLoginScreenState extends State { SizedBox(height: gap / 2), InteractiveScale( child: OutlinedButton( - onPressed: () { - showTopAlert( - context, - 'Connecting with Pozo Support... Please wait.', - icon: Icons.support_agent_outlined, - ); + onPressed: () async { + AppFeedback.click(); + final Uri uri = Uri.parse('tel:7324000011'); + try { + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + if (!context.mounted) return; + showTopAlert( + context, + 'Could not launch dialer. Call 7324000011', + isError: true, + ); + } + } catch (e) { + if (!context.mounted) return; + showTopAlert( + context, + 'Dialer not available in this environment. Call 7324000011', + isError: true, + ); + } }, style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(56), diff --git a/flutter_app/lib/screens/otp_verification_screen.dart b/flutter_app/lib/screens/otp_verification_screen.dart index 53283b4..f7f2d4a 100644 --- a/flutter_app/lib/screens/otp_verification_screen.dart +++ b/flutter_app/lib/screens/otp_verification_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import '../theme.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; import 'business_setup_screen.dart'; class OtpVerificationScreen extends StatefulWidget { @@ -50,6 +51,21 @@ class _OtpVerificationScreenState extends State { super.dispose(); } + void _verifyOtp() { + final code = _controllers.map((c) => c.text.trim()).join(); + if (code.length < 6) { + AppFeedback.error(); + showTopAlert(context, 'Please enter a valid 6-digit OTP code.', isError: true); + return; + } + AppFeedback.success(); + showTopAlert(context, 'OTP Verified Successfully!'); + Navigator.pushReplacement( + context, + FadeSlidePageRoute(child: const BusinessSetupScreen()), + ); + } + String _getFormattedTime() { return '00:${_timeLeft.toString().padLeft(2, '0')}'; } @@ -67,7 +83,10 @@ class _OtpVerificationScreenState extends State { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)), - onPressed: () => Navigator.pop(context), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, ), title: Text( 'PozoApp', @@ -182,7 +201,10 @@ class _OtpVerificationScreenState extends State { ), const SizedBox(width: 8), InteractiveScale( - onTap: () => Navigator.pop(context), + onTap: () { + AppFeedback.click(); + Navigator.pop(context); + }, child: Text( 'Edit', style: TextStyle( @@ -233,8 +255,15 @@ class _OtpVerificationScreenState extends State { ), ), onChanged: (value) { - if (value.isNotEmpty && fieldIndex < 5) { - _focusNodes[fieldIndex + 1].requestFocus(); + AppFeedback.subtleHaptic(); + if (value.isNotEmpty) { + if (fieldIndex < 5) { + _focusNodes[fieldIndex + 1].requestFocus(); + } else { + _verifyOtp(); + } + } else if (value.isEmpty && fieldIndex > 0) { + _focusNodes[fieldIndex - 1].requestFocus(); } }, ), @@ -304,6 +333,7 @@ class _OtpVerificationScreenState extends State { child: InteractiveScale( child: OutlinedButton.icon( onPressed: () { + AppFeedback.click(); showTopAlert( context, 'WhatsApp verification request sent!', @@ -370,14 +400,7 @@ class _OtpVerificationScreenState extends State { }), InteractiveScale( child: TextButton.icon( - onPressed: () { - Navigator.push( - context, - FadeSlidePageRoute( - child: const BusinessSetupScreen(), - ), - ); - }, + onPressed: _verifyOtp, icon: const Text( 'Verify Manually', style: TextStyle(fontWeight: FontWeight.bold), @@ -407,7 +430,10 @@ class _OtpVerificationScreenState extends State { Widget _buildFooterAction(IconData icon, String label, VoidCallback onTap) { return InteractiveScale( - onTap: onTap, + onTap: () { + AppFeedback.click(); + onTap(); + }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/flutter_app/lib/screens/setup_complete_screen.dart b/flutter_app/lib/screens/setup_complete_screen.dart index 4e11171..2de5573 100644 --- a/flutter_app/lib/screens/setup_complete_screen.dart +++ b/flutter_app/lib/screens/setup_complete_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../theme.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; import 'billing_dashboard_screen.dart'; class SetupCompleteScreen extends StatefulWidget { @@ -77,6 +78,9 @@ class _SetupCompleteScreenState extends State with SingleTi ); _controller.forward(); + WidgetsBinding.instance.addPostFrameCallback((_) { + AppFeedback.success(); + }); } @override @@ -384,6 +388,7 @@ class _SetupCompleteScreenState extends State with SingleTi child: InteractiveScale( child: ElevatedButton( onPressed: () { + AppFeedback.click(); showTopAlert( context, 'Launching Register Dashboard... Enjoy billing!', diff --git a/flutter_app/lib/screens/welcome_screen.dart b/flutter_app/lib/screens/welcome_screen.dart index f185777..0d2e116 100644 --- a/flutter_app/lib/screens/welcome_screen.dart +++ b/flutter_app/lib/screens/welcome_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobile_number/mobile_number.dart'; import '../theme.dart'; import '../route_transitions.dart'; +import '../feedback_helper.dart'; import 'mobile_login_screen.dart'; class WelcomeScreen extends StatefulWidget { @@ -18,9 +21,12 @@ class _WelcomeScreenState extends State with SingleTickerProvider late Animation _contentFadeAnimation; late Animation _contentSlideAnimation; + String? _detectedMobileNumber; + @override void initState() { super.initState(); + _initMobileNumberDetection(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1400), @@ -72,6 +78,30 @@ class _WelcomeScreenState extends State with SingleTickerProvider _controller.forward(); } + Future _initMobileNumberDetection() async { + try { + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + if (await MobileNumber.hasPhonePermission) { + final String? mobileNumber = await MobileNumber.mobileNumber; + if (mobileNumber != null && mobileNumber.isNotEmpty) { + // Extract 10 digits if possible, else just use the number + final cleaned = mobileNumber.replaceAll(RegExp(r'\D'), ''); + if (cleaned.length >= 10) { + _detectedMobileNumber = cleaned.substring(cleaned.length - 10); + } else { + _detectedMobileNumber = mobileNumber; + } + return; + } + } + } + } catch (e) { + debugPrint('Error getting mobile number: $e'); + } + // Fallback to mock number + _detectedMobileNumber = '9988776655'; + } + @override void dispose() { _controller.dispose(); @@ -493,9 +523,10 @@ class _WelcomeScreenState extends State with SingleTickerProvider InteractiveScale( child: ElevatedButton( onPressed: () { + AppFeedback.click(); Navigator.push( context, - FadeSlidePageRoute(child: const MobileLoginScreen()), + FadeSlidePageRoute(child: MobileLoginScreen(autoFilledMobileNumber: _detectedMobileNumber)), ); }, style: ElevatedButton.styleFrom( diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock index 53d805e..c82a998 100644 --- a/flutter_app/pubspec.lock +++ b/flutter_app/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -9,6 +17,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.dev" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.dev" + source: hosted + version: "3.1.0" boolean_selector: dependency: transitive description: @@ -65,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" fake_async: dependency: transitive description: @@ -81,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" file_selector_linux: dependency: transitive description: @@ -113,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -126,6 +230,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.9+2" + flutter_haptic_feedback: + dependency: "direct main" + description: + name: flutter_haptic_feedback + sha256: e6a1ac6b5119c9b0964e4de2eaa6bfb66facedc6e33bfe2a902da3f6f9666499 + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter_lints: dependency: "direct dev" description: @@ -152,6 +264,102 @@ packages: description: flutter source: sdk version: "0.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: dde05dae7d584db6e82feb87dd9fb0b4b4c83ed68065667b4bef637be38e13a7 + url: "https://pub.dev" + source: hosted + version: "4.2.7" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" google_fonts: dependency: "direct main" description: @@ -160,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" http: dependency: transitive description: @@ -240,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -304,6 +528,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_number: + dependency: "direct main" + description: + name: mobile_number + sha256: "8dc5b7a19193f7a41fa07aa933c8db8085033e6201b20323f20e4d283ea2a9a8" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -364,18 +612,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.3" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: @@ -408,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -461,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -485,6 +749,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" + url: "https://pub.dev" + source: hosted + version: "6.3.20" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: @@ -493,6 +829,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vibration: + dependency: "direct main" + description: + name: vibration + sha256: "06588a845a4ebc73ab7ff7da555c2b3dbcd9676164b5856a38bf0b2287f1045d" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + vibration_platform_interface: + dependency: transitive + description: + name: vibration_platform_interface + sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36" + url: "https://pub.dev" + source: hosted + version: "0.0.3" vm_service: dependency: transitive description: @@ -509,6 +861,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" xdg_directories: dependency: transitive description: @@ -517,6 +885,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.8.0 <4.0.0" flutter: ">=3.32.0" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 4f76fa0..ab87a0c 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -10,9 +10,16 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.5 google_fonts: ^5.1.0 - image_picker: ^1.0.4 - permission_handler: ^11.0.1 + image_picker: ^1.2.1 + permission_handler: ^12.0.3 flutter_contacts: ^1.1.9 + geolocator: ^14.0.2 + geocoding: ^4.0.0 + url_launcher: ^6.3.2 + mobile_number: ^3.0.0 + vibration: ^1.8.4 + audioplayers: ^5.2.1 + flutter_haptic_feedback: ^1.0.1 dev_dependencies: flutter_test: @@ -23,3 +30,4 @@ flutter: uses-material-design: true assets: - assets/logo/ + - assets/sounds/