feat: add sound and haptic feedback throughout onboarding and setup flow

This commit is contained in:
Sridhar R 2026-06-10 17:43:21 +05:30
parent 3f2a804a31
commit a3db70ffc6
16 changed files with 882 additions and 68 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -51,5 +51,13 @@
<string>PozoApp requires photo gallery access to upload documents and shop facade images.</string> <string>PozoApp requires photo gallery access to upload documents and shop facade images.</string>
<key>NSContactsUsageDescription</key> <key>NSContactsUsageDescription</key>
<string>PozoApp requires contacts access to let you search and auto-fill mobile numbers during registration and billing.</string> <string>PozoApp requires contacts access to let you search and auto-fill mobile numbers during registration and billing.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>PozoApp needs your location to automatically fetch and verify your shop's address.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>PozoApp needs your location to automatically fetch and verify your shop's address.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>tel</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -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<void> 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<void> _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();
}
}
}

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'feedback_helper.dart';
class FadeSlidePageRoute<T> extends PageRouteBuilder<T> { class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
final Widget child; final Widget child;
@ -33,7 +34,9 @@ class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
}, },
transitionDuration: const Duration(milliseconds: 300), transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 200), reverseTransitionDuration: const Duration(milliseconds: 200),
); ) {
AppFeedback.subtleHaptic();
}
} }
void showTopAlert( void showTopAlert(

View File

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
class ProductItem { class ProductItem {
final String id; final String id;
@ -136,6 +137,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
} }
void _simulateBarcodeScan() { void _simulateBarcodeScan() {
AppFeedback.click();
if (!widget.barcodeScanner) return; if (!widget.barcodeScanner) return;
final random = Random(); final random = Random();
final randomProduct = _products[random.nextInt(_products.length)]; final randomProduct = _products[random.nextInt(_products.length)];
@ -177,6 +179,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
double get _total => _subtotal + _cgst + _sgst; double get _total => _subtotal + _cgst + _sgst;
void _handleReviewAndGenerate() { void _handleReviewAndGenerate() {
AppFeedback.success();
final random = Random(); final random = Random();
final invoiceVal = 'POZO-${random.nextInt(90000) + 10000}'; final invoiceVal = 'POZO-${random.nextInt(90000) + 10000}';
setState(() { setState(() {
@ -186,6 +189,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
} }
void _closeReceiptAndReset() { void _closeReceiptAndReset() {
AppFeedback.click();
setState(() { setState(() {
_printingInvoice = false; _printingInvoice = false;
_billingList.clear(); _billingList.clear();
@ -206,7 +210,10 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
elevation: 4, elevation: 4,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context), onPressed: () {
AppFeedback.click();
Navigator.pop(context);
},
), ),
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -964,6 +971,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
InteractiveScale( InteractiveScale(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
AppFeedback.click();
showTopAlert( showTopAlert(
context, context,
'Thermal Printed: $_invoiceNumber successfully!', 'Thermal Printed: $_invoiceNumber successfully!',

View File

@ -2,9 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';
import 'dart:ui'; import 'dart:ui';
import 'dart:io';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
import 'customize_setup_screen.dart'; import 'customize_setup_screen.dart';
class BusinessSetupScreen extends StatefulWidget { class BusinessSetupScreen extends StatefulWidget {
@ -17,10 +21,16 @@ class BusinessSetupScreen extends StatefulWidget {
class _BusinessSetupScreenState extends State<BusinessSetupScreen> { class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _gstinController = TextEditingController(); final TextEditingController _gstinController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
String _selectedCategory = 'supermarket'; String _selectedCategory = 'supermarket';
bool _isNameValid = false; bool _isNameValid = false;
bool _isGstinValid = false;
bool _hasGstinInput = false;
final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'}; final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'};
final Map<String, String> _uploadedDocs = {}; final Map<String, XFile> _uploadedDocs = {};
@override @override
void initState() { void initState() {
@ -30,12 +40,30 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
_isNameValid = _nameController.text.trim().isNotEmpty; _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 @override
void dispose() { void dispose() {
_nameController.dispose(); _nameController.dispose();
_gstinController.dispose(); _gstinController.dispose();
_addressController.dispose();
super.dispose(); super.dispose();
} }
@ -49,10 +77,16 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
]; ];
Future<void> _handleImagePick(String docName, ImageSource source) async { Future<void> _handleImagePick(String docName, ImageSource source) async {
AppFeedback.click();
final permission = source == ImageSource.camera ? Permission.camera : Permission.photos; final permission = source == ImageSource.camera ? Permission.camera : Permission.photos;
var status = await permission.status; var status = await permission.status;
if (!mounted) return; if (!mounted) return;
if (status.isPermanentlyDenied) {
_showSettingsDialog(source == ImageSource.camera ? 'Camera' : 'Photos');
return;
}
if (status.isDenied && !kIsWeb) { if (status.isDenied && !kIsWeb) {
final String sourceName = source == ImageSource.camera ? 'Camera' : 'Photos/Gallery'; final String sourceName = source == ImageSource.camera ? 'Camera' : 'Photos/Gallery';
final bool? proceed = await showDialog<bool>( final bool? proceed = await showDialog<bool>(
@ -64,11 +98,17 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () {
AppFeedback.click();
Navigator.pop(context, false);
},
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () {
AppFeedback.click();
Navigator.pop(context, true);
},
child: const Text('Continue'), child: const Text('Continue'),
), ),
], ],
@ -79,6 +119,11 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
if (proceed != true) return; if (proceed != true) return;
status = await permission.request(); status = await permission.request();
if (!mounted) return; if (!mounted) return;
if (status.isPermanentlyDenied) {
_showSettingsDialog(sourceName);
return;
}
} }
if (status.isGranted || kIsWeb) { if (status.isGranted || kIsWeb) {
@ -88,7 +133,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
if (!mounted) return; if (!mounted) return;
if (file != null) { if (file != null) {
setState(() { setState(() {
_uploadedDocs[docName] = file.name; _uploadedDocs[docName] = file;
}); });
showTopAlert( showTopAlert(
context, context,
@ -113,6 +158,110 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
} }
} }
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<void> _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<Placemark> placemarks = await placemarkFromCoordinates(
position.latitude, position.longitude);
if (placemarks.isNotEmpty) {
Placemark place = placemarks[0];
List<String> 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() { void _showFacadePhotoOptions() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -124,6 +273,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)), leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)),
title: const Text('Take Photo'), title: const Text('Take Photo'),
onTap: () { onTap: () {
AppFeedback.click();
Navigator.pop(context); Navigator.pop(context);
_handleImagePick('facade_photo', ImageSource.camera); _handleImagePick('facade_photo', ImageSource.camera);
}, },
@ -132,6 +282,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)), leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)),
title: const Text('Choose from Gallery'), title: const Text('Choose from Gallery'),
onTap: () { onTap: () {
AppFeedback.click();
Navigator.pop(context); Navigator.pop(context);
_handleImagePick('facade_photo', ImageSource.gallery); _handleImagePick('facade_photo', ImageSource.gallery);
}, },
@ -149,13 +300,16 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
return InteractiveScale( return InteractiveScale(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
setState(() { AppFeedback.lightHaptic();
if (isSelected) { if (!isSelected) {
_selectedDocs.add(label);
_showFacadePhotoOptionsForDoc(label);
} else {
setState(() {
_selectedDocs.remove(label); _selectedDocs.remove(label);
} else { _uploadedDocs.remove(label);
_selectedDocs.add(label); });
} }
});
}, },
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
@ -193,6 +347,38 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
); );
} }
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) { Widget _buildUploadSection(String docName, IconData icon) {
final theme = Theme.of(context); final theme = Theme.of(context);
final isUploaded = _uploadedDocs.containsKey(docName); final isUploaded = _uploadedDocs.containsKey(docName);
@ -231,6 +417,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
IconButton( IconButton(
icon: const Icon(Icons.close, size: 18, color: Colors.grey), icon: const Icon(Icons.close, size: 18, color: Colors.grey),
onPressed: () { onPressed: () {
AppFeedback.click();
setState(() { setState(() {
_selectedDocs.remove(docName); _selectedDocs.remove(docName);
}); });
@ -258,13 +445,20 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
color: Colors.red[50], color: Colors.red[50],
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Center( clipBehavior: Clip.antiAlias,
child: Icon( child: kIsWeb
Icons.insert_drive_file, ? Image.network(
color: Color(0xFFAF101A), _uploadedDocs[docName]!.path,
size: 24, 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), const SizedBox(width: 12),
Expanded( Expanded(
@ -272,7 +466,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_uploadedDocs[docName] ?? 'Attached_document.pdf', _uploadedDocs[docName]?.name ?? 'Attached_document.jpg',
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -293,6 +487,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red), icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () { onPressed: () {
AppFeedback.click();
setState(() { setState(() {
_uploadedDocs.remove(docName); _uploadedDocs.remove(docName);
}); });
@ -392,7 +587,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)), icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
onPressed: () => Navigator.pop(context), onPressed: () {
AppFeedback.click();
Navigator.pop(context);
},
), ),
title: Text( title: Text(
'PozoApp', 'PozoApp',
@ -523,6 +721,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
return InteractiveScale( return InteractiveScale(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
AppFeedback.select();
setState(() { setState(() {
_selectedCategory = cat['id']; _selectedCategory = cat['id'];
}); });
@ -612,8 +811,14 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
child: TextField( child: TextField(
controller: _gstinController, controller: _gstinController,
textCapitalization: TextCapitalization.characters, textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration( decoration: InputDecoration(
hintText: '15-DIGIT GST NUMBER', 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), style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
), ),
@ -622,11 +827,21 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
InteractiveScale( InteractiveScale(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
showTopAlert( if (_isGstinValid) {
context, AppFeedback.success();
'GSTIN Verified Successfully!', showTopAlert(
icon: Icons.verified_outlined, 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( style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48), minimumSize: const Size(0, 48),
@ -667,13 +882,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
InteractiveScale( InteractiveScale(
child: InkWell( child: InkWell(
onTap: () { onTap: _fetchLocationAndAddress,
showTopAlert(
context,
'Location detected: Sector 62, Noida, UP',
icon: Icons.my_location_outlined,
);
},
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
@ -731,6 +940,23 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
), ),
), ),
), ),
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<BusinessSetupScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( 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( style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
@ -867,6 +1093,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isNameValid onPressed: _isNameValid
? () { ? () {
AppFeedback.click();
Navigator.push( Navigator.push(
context, context,
FadeSlidePageRoute( FadeSlidePageRoute(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
import 'setup_complete_screen.dart'; import 'setup_complete_screen.dart';
class CustomizeSetupScreen extends StatefulWidget { class CustomizeSetupScreen extends StatefulWidget {
@ -33,7 +34,10 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)), icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
onPressed: () => Navigator.pop(context), onPressed: () {
AppFeedback.click();
Navigator.pop(context);
},
), ),
title: Row( title: Row(
children: const [ children: const [
@ -52,6 +56,7 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
AppFeedback.click();
Navigator.push( Navigator.push(
context, context,
FadeSlidePageRoute( FadeSlidePageRoute(
@ -194,6 +199,7 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
child: InteractiveScale( child: InteractiveScale(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
AppFeedback.click();
Navigator.push( Navigator.push(
context, context,
FadeSlidePageRoute( FadeSlidePageRoute(
@ -235,8 +241,13 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
ThemeData theme, ThemeData theme,
) { ) {
void wrappedOnChanged(bool val) {
AppFeedback.click();
onChanged(val);
}
return InteractiveScale( return InteractiveScale(
onTap: () => onChanged(!value), onTap: () => wrappedOnChanged(!value),
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -290,7 +301,7 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
), ),
Switch( Switch(
value: value, value: value,
onChanged: onChanged, onChanged: wrappedOnChanged,
activeColor: Colors.white, activeColor: Colors.white,
activeTrackColor: theme.colorScheme.primary, activeTrackColor: theme.colorScheme.primary,
), ),

View File

@ -3,8 +3,10 @@ import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
import 'otp_verification_screen.dart'; import 'otp_verification_screen.dart';
class MockContact { class MockContact {
@ -21,19 +23,23 @@ class MockPhone {
} }
class MobileLoginScreen extends StatefulWidget { class MobileLoginScreen extends StatefulWidget {
const MobileLoginScreen({super.key}); final String? autoFilledMobileNumber;
const MobileLoginScreen({super.key, this.autoFilledMobileNumber});
@override @override
State<MobileLoginScreen> createState() => _MobileLoginScreenState(); State<MobileLoginScreen> createState() => _MobileLoginScreenState();
} }
class _MobileLoginScreenState extends State<MobileLoginScreen> { class _MobileLoginScreenState extends State<MobileLoginScreen> {
final TextEditingController _controller = TextEditingController(); late final TextEditingController _controller;
bool _isValid = false; bool _isValid = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = TextEditingController(text: widget.autoFilledMobileNumber ?? '');
_isValid = _controller.text.length == 10;
_controller.addListener(_validateInput); _controller.addListener(_validateInput);
} }
@ -50,6 +56,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
} }
Future<void> _pickContact() async { Future<void> _pickContact() async {
AppFeedback.click();
const permission = Permission.contacts; const permission = Permission.contacts;
var status = await permission.status; var status = await permission.status;
if (!mounted) return; if (!mounted) return;
@ -195,6 +202,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
), ),
), ),
onTap: () { onTap: () {
AppFeedback.click();
_selectContactNumber(phone); _selectContactNumber(phone);
Navigator.pop(context); Navigator.pop(context);
}, },
@ -207,7 +215,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () {
AppFeedback.click();
Navigator.pop(context);
},
child: const Text('Close'), child: const Text('Close'),
) )
], ],
@ -227,7 +238,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)), icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)),
onPressed: () => Navigator.pop(context), onPressed: () {
AppFeedback.click();
Navigator.pop(context);
},
), ),
title: Text( title: Text(
'PozoApp', 'PozoApp',
@ -409,6 +423,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
AppFeedback.click();
_controller.text = '9876543210'; _controller.text = '9876543210';
setState(() { setState(() {
_isValid = true; _isValid = true;
@ -605,6 +620,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isValid onPressed: _isValid
? () { ? () {
AppFeedback.click();
Navigator.push( Navigator.push(
context, context,
FadeSlidePageRoute( FadeSlidePageRoute(
@ -630,12 +646,28 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
SizedBox(height: gap / 2), SizedBox(height: gap / 2),
InteractiveScale( InteractiveScale(
child: OutlinedButton( child: OutlinedButton(
onPressed: () { onPressed: () async {
showTopAlert( AppFeedback.click();
context, final Uri uri = Uri.parse('tel:7324000011');
'Connecting with Pozo Support... Please wait.', try {
icon: Icons.support_agent_outlined, 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( style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(56), minimumSize: const Size.fromHeight(56),

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
import 'business_setup_screen.dart'; import 'business_setup_screen.dart';
class OtpVerificationScreen extends StatefulWidget { class OtpVerificationScreen extends StatefulWidget {
@ -50,6 +51,21 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
super.dispose(); 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() { String _getFormattedTime() {
return '00:${_timeLeft.toString().padLeft(2, '0')}'; return '00:${_timeLeft.toString().padLeft(2, '0')}';
} }
@ -67,7 +83,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)), icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
onPressed: () => Navigator.pop(context), onPressed: () {
AppFeedback.click();
Navigator.pop(context);
},
), ),
title: Text( title: Text(
'PozoApp', 'PozoApp',
@ -182,7 +201,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
InteractiveScale( InteractiveScale(
onTap: () => Navigator.pop(context), onTap: () {
AppFeedback.click();
Navigator.pop(context);
},
child: Text( child: Text(
'Edit', 'Edit',
style: TextStyle( style: TextStyle(
@ -233,8 +255,15 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
), ),
), ),
onChanged: (value) { onChanged: (value) {
if (value.isNotEmpty && fieldIndex < 5) { AppFeedback.subtleHaptic();
_focusNodes[fieldIndex + 1].requestFocus(); 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<OtpVerificationScreen> {
child: InteractiveScale( child: InteractiveScale(
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () { onPressed: () {
AppFeedback.click();
showTopAlert( showTopAlert(
context, context,
'WhatsApp verification request sent!', 'WhatsApp verification request sent!',
@ -370,14 +400,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
}), }),
InteractiveScale( InteractiveScale(
child: TextButton.icon( child: TextButton.icon(
onPressed: () { onPressed: _verifyOtp,
Navigator.push(
context,
FadeSlidePageRoute(
child: const BusinessSetupScreen(),
),
);
},
icon: const Text( icon: const Text(
'Verify Manually', 'Verify Manually',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
@ -407,7 +430,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
Widget _buildFooterAction(IconData icon, String label, VoidCallback onTap) { Widget _buildFooterAction(IconData icon, String label, VoidCallback onTap) {
return InteractiveScale( return InteractiveScale(
onTap: onTap, onTap: () {
AppFeedback.click();
onTap();
},
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
import 'billing_dashboard_screen.dart'; import 'billing_dashboard_screen.dart';
class SetupCompleteScreen extends StatefulWidget { class SetupCompleteScreen extends StatefulWidget {
@ -77,6 +78,9 @@ class _SetupCompleteScreenState extends State<SetupCompleteScreen> with SingleTi
); );
_controller.forward(); _controller.forward();
WidgetsBinding.instance.addPostFrameCallback((_) {
AppFeedback.success();
});
} }
@override @override
@ -384,6 +388,7 @@ class _SetupCompleteScreenState extends State<SetupCompleteScreen> with SingleTi
child: InteractiveScale( child: InteractiveScale(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
AppFeedback.click();
showTopAlert( showTopAlert(
context, context,
'Launching Register Dashboard... Enjoy billing!', 'Launching Register Dashboard... Enjoy billing!',

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_number/mobile_number.dart';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import '../feedback_helper.dart';
import 'mobile_login_screen.dart'; import 'mobile_login_screen.dart';
class WelcomeScreen extends StatefulWidget { class WelcomeScreen extends StatefulWidget {
@ -18,9 +21,12 @@ class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProvider
late Animation<double> _contentFadeAnimation; late Animation<double> _contentFadeAnimation;
late Animation<Offset> _contentSlideAnimation; late Animation<Offset> _contentSlideAnimation;
String? _detectedMobileNumber;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initMobileNumberDetection();
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 1400), duration: const Duration(milliseconds: 1400),
@ -72,6 +78,30 @@ class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProvider
_controller.forward(); _controller.forward();
} }
Future<void> _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 @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
@ -493,9 +523,10 @@ class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProvider
InteractiveScale( InteractiveScale(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
AppFeedback.click();
Navigator.push( Navigator.push(
context, context,
FadeSlidePageRoute(child: const MobileLoginScreen()), FadeSlidePageRoute(child: MobileLoginScreen(autoFilledMobileNumber: _detectedMobileNumber)),
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -9,6 +17,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -65,6 +129,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -81,6 +169,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -113,6 +209,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+5" version: "0.9.3+5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -126,6 +230,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.9+2" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -152,6 +264,102 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -160,6 +368,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.0" version: "5.1.0"
gsettings:
dependency: transitive
description:
name: gsettings
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
url: "https://pub.dev"
source: hosted
version: "0.2.8"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -240,6 +456,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -304,6 +528,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" 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: path:
dependency: transitive dependency: transitive
description: description:
@ -364,18 +612,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.4.0" version: "12.0.3"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.1.0" version: "13.0.1"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@ -408,6 +656,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1" version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -461,6 +717,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -485,6 +749,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -493,6 +829,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -509,6 +861,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -517,6 +885,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.0" flutter: ">=3.32.0"

View File

@ -10,9 +10,16 @@ dependencies:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
google_fonts: ^5.1.0 google_fonts: ^5.1.0
image_picker: ^1.0.4 image_picker: ^1.2.1
permission_handler: ^11.0.1 permission_handler: ^12.0.3
flutter_contacts: ^1.1.9 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: dev_dependencies:
flutter_test: flutter_test:
@ -23,3 +30,4 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/logo/ - assets/logo/
- assets/sounds/