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