Refactor contacts using flutter_contacts, unify InteractiveScale animations and 16px corner radius across welcome and setup complete screens, and compile final release APK

This commit is contained in:
Sridhar R 2026-06-10 14:47:50 +05:30
parent ac1294a150
commit 3f2a804a31
12 changed files with 1580 additions and 1256 deletions

View File

@ -11,6 +11,28 @@ rootProject.layout.buildDirectory.value(newBuildDir)
subprojects { subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir) project.layout.buildDirectory.value(newSubprojectBuildDir)
afterEvaluate {
if (extensions.findByName("android") != null) {
val android = extensions.findByName("android") as com.android.build.gradle.BaseExtension
if (android.namespace == null) {
var manifestPackage: String? = null
val manifestFile = project.projectDir.resolve("src/main/AndroidManifest.xml")
if (manifestFile.exists()) {
val content = manifestFile.readText()
val match = Regex("package=\"([^\"]+)\"").find(content)
if (match != null) {
manifestPackage = match.groupValues[1]
}
}
if (manifestPackage != null) {
android.namespace = manifestPackage
} else {
android.namespace = "co.gittouch." + project.name.replace("-", "_")
}
}
}
}
} }
subprojects { subprojects {
project.evaluationDependsOn(":app") project.evaluationDependsOn(":app")

View File

@ -186,3 +186,119 @@ class _TopAlertWidgetState extends State<_TopAlertWidget> with SingleTickerProvi
); );
} }
} }
class SlideFadeEntrance extends StatefulWidget {
final Widget child;
final Duration delay;
final Duration duration;
const SlideFadeEntrance({
super.key,
required this.child,
this.delay = Duration.zero,
this.duration = const Duration(milliseconds: 500),
});
@override
State<SlideFadeEntrance> createState() => _SlideFadeEntranceState();
}
class _SlideFadeEntranceState extends State<SlideFadeEntrance> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacityAnimation;
late Animation<double> _slideAnimation;
Timer? _timer;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
_slideAnimation = Tween<double>(begin: 12.0, end: 0.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
);
if (widget.delay == Duration.zero) {
_controller.forward();
} else {
_timer = Timer(widget.delay, () {
if (mounted) {
_controller.forward();
}
});
}
}
@override
void dispose() {
_timer?.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: Transform.translate(
offset: Offset(0.0, _slideAnimation.value),
child: child,
),
);
},
child: widget.child,
);
}
}
class InteractiveScale extends StatefulWidget {
final Widget child;
final VoidCallback? onTap;
final double hoverScale;
final double tapScale;
const InteractiveScale({
super.key,
required this.child,
this.onTap,
this.hoverScale = 1.03,
this.tapScale = 0.96,
});
@override
State<InteractiveScale> createState() => _InteractiveScaleState();
}
class _InteractiveScaleState extends State<InteractiveScale> {
double _scale = 1.0;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _scale = widget.hoverScale),
onExit: (_) => setState(() => _scale = 1.0),
child: AnimatedScale(
scale: _scale,
duration: const Duration(milliseconds: 120),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) => setState(() => _scale = widget.tapScale),
onTapUp: (_) => setState(() => _scale = widget.hoverScale),
onTapCancel: () => setState(() => _scale = 1.0),
onTap: widget.onTap,
child: widget.child,
),
),
);
}
}

View File

@ -248,21 +248,23 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Directionality( child: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: ElevatedButton.icon( child: InteractiveScale(
onPressed: _simulateBarcodeScan, child: ElevatedButton.icon(
style: ElevatedButton.styleFrom( onPressed: _simulateBarcodeScan,
minimumSize: const Size(0, 32), style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary, minimumSize: const Size(0, 32),
foregroundColor: Colors.white, backgroundColor: theme.colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), foregroundColor: Colors.white,
shape: RoundedRectangleBorder( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
borderRadius: BorderRadius.circular(12), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.qr_code_scanner, size: 16),
label: const Text(
'Scan Item',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
), ),
),
icon: const Icon(Icons.qr_code_scanner, size: 16),
label: const Text(
'Scan Item',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
), ),
), ),
), ),
@ -407,95 +409,103 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
final isLowStock = widget.inventoryAlerts && remainingStock <= 3; final isLowStock = widget.inventoryAlerts && remainingStock <= 3;
final isOutOfStock = widget.inventoryAlerts && remainingStock <= 0; final isOutOfStock = widget.inventoryAlerts && remainingStock <= 0;
return InkWell( return SlideFadeEntrance(
onTap: isOutOfStock ? null : () => _handleAddItem(prod), delay: Duration(milliseconds: index * 40),
borderRadius: BorderRadius.circular(16), duration: const Duration(milliseconds: 300),
child: Container( child: InteractiveScale(
padding: const EdgeInsets.all(10), tapScale: isOutOfStock ? 1.0 : 0.95,
decoration: BoxDecoration( hoverScale: isOutOfStock ? 1.0 : 1.03,
color: countInCart > 0 child: InkWell(
? const Color(0xFF1E1B4B) // Deep indigo accent onTap: isOutOfStock ? null : () => _handleAddItem(prod),
: const Color(0xFF1E293B).withOpacity(0.8),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( child: Container(
color: countInCart > 0 ? theme.colorScheme.primary : const Color(0xFF334155), padding: const EdgeInsets.all(10),
width: countInCart > 0 ? 1.5 : 1.0, decoration: BoxDecoration(
), color: countInCart > 0
), ? const Color(0xFF1E1B4B) // Deep indigo accent
child: Column( : const Color(0xFF1E293B).withOpacity(0.8),
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(16),
mainAxisAlignment: MainAxisAlignment.spaceBetween, border: Border.all(
children: [ color: countInCart > 0 ? theme.colorScheme.primary : const Color(0xFF334155),
Expanded( width: countInCart > 0 ? 1.5 : 1.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
prod.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
const SizedBox(height: 4),
Text(
'${prod.price.toStringAsFixed(0)}',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
],
), ),
), ),
Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (widget.inventoryAlerts) Expanded(
Container( child: Column(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), crossAxisAlignment: CrossAxisAlignment.start,
decoration: BoxDecoration( children: [
color: isLowStock Text(
? Colors.red.withOpacity(0.15) prod.name,
: const Color(0xFF334155), maxLines: 2,
borderRadius: BorderRadius.circular(6), overflow: TextOverflow.ellipsis,
), style: const TextStyle(
child: Text( color: Colors.white,
isOutOfStock ? 'Out of stock' : '$remainingStock Stock', fontWeight: FontWeight.bold,
style: TextStyle( fontSize: 11,
color: isLowStock ? Colors.redAccent : Colors.grey, ),
fontSize: 8,
fontWeight: FontWeight.bold,
), ),
), const SizedBox(height: 4),
) Text(
else '${prod.price.toStringAsFixed(0)}',
const SizedBox(), style: TextStyle(
if (countInCart > 0) color: theme.colorScheme.primary,
Container( fontWeight: FontWeight.w800,
width: 20, fontSize: 13,
height: 20, ),
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'$countInCart',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
), ),
), ],
) ),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.inventoryAlerts)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isLowStock
? Colors.red.withOpacity(0.15)
: const Color(0xFF334155),
borderRadius: BorderRadius.circular(6),
),
child: Text(
isOutOfStock ? 'Out of stock' : '$remainingStock Stock',
style: TextStyle(
color: isLowStock ? Colors.redAccent : Colors.grey,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
)
else
const SizedBox(),
if (countInCart > 0)
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'$countInCart',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
)
],
)
], ],
) ),
], ),
), ),
), ),
); );
@ -655,20 +665,27 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
const Divider(color: Color(0xFF334155), height: 16), const Divider(color: Color(0xFF334155), height: 16),
_buildComputationCalculations(theme), _buildComputationCalculations(theme),
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( InteractiveScale(
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate, hoverScale: _billingList.isNotEmpty ? 1.03 : 1.0,
style: ElevatedButton.styleFrom( tapScale: _billingList.isNotEmpty ? 0.96 : 1.0,
backgroundColor: theme.colorScheme.primary, child: ElevatedButton(
disabledBackgroundColor: const Color(0xFF1E293B), onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
minimumSize: const Size.fromHeight(48), style: ElevatedButton.styleFrom(
), backgroundColor: theme.colorScheme.primary,
child: const Row( disabledBackgroundColor: const Color(0xFF1E293B),
mainAxisAlignment: MainAxisAlignment.center, minimumSize: const Size.fromHeight(48),
children: [ shape: RoundedRectangleBorder(
Icon(Icons.receipt, size: 16), borderRadius: BorderRadius.circular(16),
SizedBox(width: 8), ),
Text('Generate Invoice'), ),
], child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt, size: 16),
SizedBox(width: 8),
Text('Generate Invoice'),
],
),
), ),
) )
] ]
@ -751,19 +768,26 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
) )
], ],
), ),
ElevatedButton( InteractiveScale(
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate, hoverScale: _billingList.isNotEmpty ? 1.03 : 1.0,
style: ElevatedButton.styleFrom( tapScale: _billingList.isNotEmpty ? 0.96 : 1.0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: ElevatedButton(
minimumSize: const Size(180, 44), onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
), style: ElevatedButton.styleFrom(
child: const Row( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
mainAxisSize: MainAxisSize.min, minimumSize: const Size(180, 44),
children: [ shape: RoundedRectangleBorder(
Icon(Icons.print, size: 16), borderRadius: BorderRadius.circular(16),
SizedBox(width: 8), ),
Text('Review & Invoice', style: TextStyle(fontSize: 12)), ),
], child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.print, size: 16),
SizedBox(width: 8),
Text('Review & Invoice', style: TextStyle(fontSize: 12)),
],
),
), ),
) )
], ],
@ -937,30 +961,40 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( InteractiveScale(
onPressed: () { child: ElevatedButton.icon(
showTopAlert( onPressed: () {
context, showTopAlert(
'Thermal Printed: $_invoiceNumber successfully!', context,
icon: Icons.print, 'Thermal Printed: $_invoiceNumber successfully!',
); icon: Icons.print,
}, );
icon: const Icon(Icons.print, size: 16), },
label: const Text('Simulate Print Receipt', style: TextStyle(fontSize: 12)), icon: const Icon(Icons.print, size: 16),
style: ElevatedButton.styleFrom( label: const Text('Simulate Print Receipt', style: TextStyle(fontSize: 12)),
backgroundColor: Colors.black, style: ElevatedButton.styleFrom(
foregroundColor: Colors.white, backgroundColor: Colors.black,
minimumSize: const Size.fromHeight(42), foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(42),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
ElevatedButton( InteractiveScale(
onPressed: _closeReceiptAndReset, child: ElevatedButton(
style: ElevatedButton.styleFrom( onPressed: _closeReceiptAndReset,
backgroundColor: theme.colorScheme.primary, style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(42), backgroundColor: theme.colorScheme.primary,
minimumSize: const Size.fromHeight(42),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('New Ledger Sale', style: TextStyle(fontSize: 12)),
), ),
child: const Text('New Ledger Sale', style: TextStyle(fontSize: 12)),
), ),
], ],
), ),

View File

@ -146,46 +146,48 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
Widget _buildDocChip(String label, IconData icon) { Widget _buildDocChip(String label, IconData icon) {
final theme = Theme.of(context); final theme = Theme.of(context);
final isSelected = _selectedDocs.contains(label); final isSelected = _selectedDocs.contains(label);
return InkWell( return InteractiveScale(
onTap: () { child: InkWell(
setState(() { onTap: () {
if (isSelected) { setState(() {
_selectedDocs.remove(label); if (isSelected) {
} else { _selectedDocs.remove(label);
_selectedDocs.add(label); } else {
} _selectedDocs.add(label);
}); }
}, });
borderRadius: BorderRadius.circular(8), },
child: Container( borderRadius: BorderRadius.circular(8),
constraints: const BoxConstraints(minHeight: 48), child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), constraints: const BoxConstraints(minHeight: 48),
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: isSelected ? const Color(0xFFFAF0F0) : Colors.white, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: isSelected ? const Color(0xFFFAF0F0) : Colors.white,
border: Border.all( borderRadius: BorderRadius.circular(8),
color: isSelected ? const Color(0xFFAF101A) : theme.colorScheme.outlineVariant, border: Border.all(
width: isSelected ? 1.5 : 1, color: isSelected ? const Color(0xFFAF101A) : theme.colorScheme.outlineVariant,
width: isSelected ? 1.5 : 1,
),
), ),
), child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Icon(
Icon( icon,
icon, size: 16,
size: 16, color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
), ),
), const SizedBox(width: 8),
], Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
),
),
],
),
), ),
), ),
); );
@ -337,31 +339,35 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
ElevatedButton.icon( InteractiveScale(
onPressed: () => _handleImagePick(docName, ImageSource.camera), child: ElevatedButton.icon(
icon: const Icon(Icons.camera_alt, size: 14), onPressed: () => _handleImagePick(docName, ImageSource.camera),
label: const Text('Take Photo'), icon: const Icon(Icons.camera_alt, size: 14),
style: ElevatedButton.styleFrom( label: const Text('Take Photo'),
minimumSize: const Size(0, 36), style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAF101A), minimumSize: const Size(0, 36),
foregroundColor: Colors.white, backgroundColor: const Color(0xFFAF101A),
elevation: 1, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), elevation: 1,
shape: RoundedRectangleBorder( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
OutlinedButton.icon( InteractiveScale(
onPressed: () => _handleImagePick(docName, ImageSource.gallery), child: OutlinedButton.icon(
icon: const Icon(Icons.photo_library, size: 14), onPressed: () => _handleImagePick(docName, ImageSource.gallery),
label: const Text('From Gallery'), icon: const Icon(Icons.photo_library, size: 14),
style: OutlinedButton.styleFrom( label: const Text('From Gallery'),
foregroundColor: const Color(0xFFAF101A), style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFAF101A)), foregroundColor: const Color(0xFFAF101A),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), side: const BorderSide(color: Color(0xFFAF101A)),
shape: RoundedRectangleBorder( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
),
), ),
), ),
], ],
@ -441,337 +447,407 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( SlideFadeEntrance(
'Complete verification', delay: Duration.zero,
style: theme.textTheme.headlineLarge?.copyWith( child: Column(
fontWeight: FontWeight.bold, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
), Text(
const SizedBox(height: 6), 'Complete verification',
Text( style: theme.textTheme.headlineLarge?.copyWith(
'Verify your business details to start selling.', fontWeight: FontWeight.bold,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'Business/Shop Name *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: _nameController,
decoration: const InputDecoration(
hintText: 'e.g. Sharma General Store',
),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
'Business Type *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2.2,
),
itemCount: _categories.length,
itemBuilder: (context, index) {
final cat = _categories[index];
final isSelected = _selectedCategory == cat['id'];
return InkWell(
onTap: () {
setState(() {
_selectedCategory = cat['id'];
});
},
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFAF101A).withOpacity(0.08)
: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.01),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
), ),
child: Column( ),
mainAxisAlignment: MainAxisAlignment.center, const SizedBox(height: 6),
crossAxisAlignment: CrossAxisAlignment.center, Text(
children: [ 'Verify your business details to start selling.',
Center( style: theme.textTheme.bodyMedium,
child: Icon( ),
cat['icon'], ],
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600], ),
size: 26, ),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Business/Shop Name *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: _nameController,
decoration: const InputDecoration(
hintText: 'e.g. Sharma General Store',
),
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 180),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Business Type *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2.2,
),
itemCount: _categories.length,
itemBuilder: (context, index) {
final cat = _categories[index];
final isSelected = _selectedCategory == cat['id'];
return InteractiveScale(
child: InkWell(
onTap: () {
setState(() {
_selectedCategory = cat['id'];
});
},
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFAF101A).withOpacity(0.08)
: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.01),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Icon(
cat['icon'],
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
size: 26,
),
),
const SizedBox(height: 6),
Center(
child: Text(
cat['name'],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
),
),
)
],
),
), ),
), ),
const SizedBox(height: 6), );
Center( },
child: Text(
cat['name'],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
),
),
)
],
),
), ),
); ],
}, ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( SlideFadeEntrance(
mainAxisAlignment: MainAxisAlignment.spaceBetween, delay: const Duration(milliseconds: 260),
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
'GSTIN', children: [
style: theme.textTheme.labelSmall?.copyWith( Row(
color: theme.colorScheme.onSurface, mainAxisAlignment: MainAxisAlignment.spaceBetween,
fontWeight: FontWeight.bold, children: [
Text(
'GSTIN',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
Text(
'(Optional)',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[500],
),
)
],
), ),
), const SizedBox(height: 8),
Text( Row(
'(Optional)', crossAxisAlignment: CrossAxisAlignment.center,
style: theme.textTheme.bodySmall?.copyWith( children: [
color: Colors.grey[500], Expanded(
child: TextField(
controller: _gstinController,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
hintText: '15-DIGIT GST NUMBER',
),
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
),
),
const SizedBox(width: 12),
InteractiveScale(
child: ElevatedButton(
onPressed: () {
showTopAlert(
context,
'GSTIN Verified Successfully!',
icon: Icons.verified_outlined,
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('Verify'),
),
),
],
), ),
) const SizedBox(height: 6),
], Text(
"Auto-fetches address & legal name on verification.",
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
),
],
),
), ),
const SizedBox(height: 8), const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.center, SlideFadeEntrance(
children: [ delay: const Duration(milliseconds: 340),
Expanded( child: Column(
child: TextField( crossAxisAlignment: CrossAxisAlignment.start,
controller: _gstinController, children: [
textCapitalization: TextCapitalization.characters, Text(
decoration: const InputDecoration( 'Shop Location *',
hintText: '15-DIGIT GST NUMBER', style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
), ),
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
), ),
), const SizedBox(height: 8),
const SizedBox(width: 12), InteractiveScale(
ElevatedButton( child: InkWell(
onPressed: () { onTap: () {
showTopAlert( showTopAlert(
context, context,
'GSTIN Verified Successfully!', 'Location detected: Sector 62, Noida, UP',
icon: Icons.verified_outlined, icon: Icons.my_location_outlined,
); );
}, },
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), child: Container(
), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: const Text('Verify'), decoration: BoxDecoration(
), color: Colors.white,
], borderRadius: BorderRadius.circular(16),
), border: Border.all(
const SizedBox(height: 6), color: theme.colorScheme.outlineVariant,
Text(
"Auto-fetches address & legal name on verification.",
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
),
const SizedBox(height: 16),
Text(
'Shop Location *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () {
showTopAlert(
context,
'Location detected: Sector 62, Noida, UP',
icon: Icons.my_location_outlined,
);
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outlineVariant,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.my_location,
color: theme.colorScheme.primary,
size: 20,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Detect current location',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 15,
),
), ),
const SizedBox(height: 2), ),
Text( child: Row(
'Pin shop address automatically', children: [
style: TextStyle( Container(
fontSize: 11, padding: const EdgeInsets.all(10),
color: Colors.grey[600], decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.my_location,
color: theme.colorScheme.primary,
size: 20,
),
), ),
), const SizedBox(width: 14),
], Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Detect current location',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const SizedBox(height: 2),
Text(
'Pin shop address automatically',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
),
Icon(
Icons.chevron_right,
color: Colors.grey[400],
),
],
),
), ),
), ),
Icon(
Icons.chevron_right,
color: Colors.grey[400],
),
],
),
),
),
const SizedBox(height: 16),
Text(
'Identity / License Documents (Select all that apply)',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildDocChip('GSTIN', Icons.description),
_buildDocChip('FSSAI', Icons.restaurant),
_buildDocChip('Aadhar', Icons.badge_outlined),
_buildDocChip('Udhyog Aadhar', Icons.domain),
_buildDocChip('PAN', Icons.credit_card),
],
),
const SizedBox(height: 8),
..._selectedDocs.map((doc) {
IconData docIcon = Icons.description;
if (doc == 'GSTIN') docIcon = Icons.description;
if (doc == 'FSSAI') docIcon = Icons.restaurant;
if (doc == 'Aadhar') docIcon = Icons.badge_outlined;
if (doc == 'Udhyog Aadhar') docIcon = Icons.domain;
if (doc == 'PAN') docIcon = Icons.credit_card;
return _buildUploadSection(doc, docIcon);
}),
const SizedBox(height: 16),
Text(
'Shop Facade Photo *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () {
_showFacadePhotoOptions();
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.outlineVariant,
width: _uploadedDocs.containsKey('facade_photo') ? 2.0 : 1.0,
), ),
), ],
child: Column( ),
mainAxisAlignment: MainAxisAlignment.center, ),
children: [ const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12), SlideFadeEntrance(
decoration: BoxDecoration( delay: const Duration(milliseconds: 420),
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green.withOpacity(0.1) : theme.colorScheme.primary.withOpacity(0.1), child: Column(
shape: BoxShape.circle, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
child: Icon( Text(
_uploadedDocs.containsKey('facade_photo') ? Icons.check_circle : Icons.add_a_photo_outlined, 'Identity / License Documents (Select all that apply)',
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.primary, style: theme.textTheme.labelSmall?.copyWith(
size: 24, color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildDocChip('GSTIN', Icons.description),
_buildDocChip('FSSAI', Icons.restaurant),
_buildDocChip('Aadhar', Icons.badge_outlined),
_buildDocChip('Udhyog Aadhar', Icons.domain),
_buildDocChip('PAN', Icons.credit_card),
],
),
],
),
),
const SizedBox(height: 8),
SlideFadeEntrance(
delay: const Duration(milliseconds: 500),
child: Column(
children: _selectedDocs.map((doc) {
IconData docIcon = Icons.description;
if (doc == 'GSTIN') docIcon = Icons.description;
if (doc == 'FSSAI') docIcon = Icons.restaurant;
if (doc == 'Aadhar') docIcon = Icons.badge_outlined;
if (doc == 'Udhyog Aadhar') docIcon = Icons.domain;
if (doc == 'PAN') docIcon = Icons.credit_card;
return _buildUploadSection(doc, docIcon);
}).toList(),
),
),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 580),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Shop Facade Photo *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InteractiveScale(
child: InkWell(
onTap: () {
_showFacadePhotoOptions();
},
borderRadius: BorderRadius.circular(16),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.outlineVariant,
width: _uploadedDocs.containsKey('facade_photo') ? 2.0 : 1.0,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green.withOpacity(0.1) : theme.colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
_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(
_uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']! : 'Upload front view',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
_uploadedDocs.containsKey('facade_photo') ? 'Facade Photo Attached • Tap to change' : 'Clear photo showing shop board',
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
),
],
),
), ),
), ),
const SizedBox(height: 12), ),
Text( ],
_uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']! : 'Upload front view',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
_uploadedDocs.containsKey('facade_photo') ? 'Facade Photo Attached • Tap to change' : 'Clear photo showing shop board',
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
),
],
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -781,30 +857,40 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
), ),
), ),
Padding( SlideFadeEntrance(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), delay: const Duration(milliseconds: 660),
child: ElevatedButton( child: Padding(
onPressed: _isNameValid padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
? () { child: InteractiveScale(
Navigator.push( hoverScale: _isNameValid ? 1.03 : 1.0,
context, tapScale: _isNameValid ? 0.96 : 1.0,
FadeSlidePageRoute( child: ElevatedButton(
child: CustomizeSetupScreen( onPressed: _isNameValid
businessName: _nameController.text.trim(), ? () {
businessType: _selectedCategory, Navigator.push(
gstin: _gstinController.text.trim(), context,
), FadeSlidePageRoute(
), child: CustomizeSetupScreen(
); businessName: _nameController.text.trim(),
} businessType: _selectedCategory,
: null, gstin: _gstinController.text.trim(),
style: ElevatedButton.styleFrom( ),
minimumSize: const Size.fromHeight(54), ),
backgroundColor: _isNameValid ? theme.colorScheme.primary : Colors.grey[200], );
foregroundColor: _isNameValid ? Colors.white : Colors.grey[400], }
elevation: _isNameValid ? 2 : 0, : null,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
backgroundColor: _isNameValid ? theme.colorScheme.primary : Colors.grey[200],
foregroundColor: _isNameValid ? Colors.white : Colors.grey[400],
elevation: _isNameValid ? 2 : 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('Continue to Verification'),
),
), ),
child: const Text('Continue to Verification'),
), ),
) )
], ],

View File

@ -119,49 +119,66 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( SlideFadeEntrance(
'Customize Your Setup', delay: Duration.zero,
style: theme.textTheme.headlineLarge?.copyWith( child: Column(
fontWeight: FontWeight.bold, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Customize Your Setup',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Turn on the features you need for your shop. You can always change these later in settings.',
style: theme.textTheme.bodyMedium,
),
],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 20),
Text(
'Turn on the features you need for your shop. You can always change these later in settings.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
// Toggle 1: Barcode // Toggle 1: Barcode
_buildToggleCard( SlideFadeEntrance(
Icons.qr_code_scanner, delay: const Duration(milliseconds: 100),
'Barcode Scanner', child: _buildToggleCard(
'Do you use a Barcode Scanner?', Icons.qr_code_scanner,
_barcode, 'Barcode Scanner',
(val) => setState(() => _barcode = val), 'Do you use a Barcode Scanner?',
theme, _barcode,
(val) => setState(() => _barcode = val),
theme,
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Toggle 2: KOT // Toggle 2: KOT
_buildToggleCard( SlideFadeEntrance(
Icons.receipt_long, delay: const Duration(milliseconds: 180),
'KOT Management', child: _buildToggleCard(
'Do you need Kitchen Order Ticket management?', Icons.receipt_long,
_kot, 'KOT Management',
(val) => setState(() => _kot = val), 'Do you need Kitchen Order Ticket management?',
theme, _kot,
(val) => setState(() => _kot = val),
theme,
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Toggle 3: Inventory stock tracking // Toggle 3: Inventory stock tracking
_buildToggleCard( SlideFadeEntrance(
Icons.inventory_2, delay: const Duration(milliseconds: 260),
'Inventory Alerts', child: _buildToggleCard(
'Get notified when stock is running low.', Icons.inventory_2,
_inventory, 'Inventory Alerts',
(val) => setState(() => _inventory = val), 'Get notified when stock is running low.',
theme, _inventory,
(val) => setState(() => _inventory = val),
theme,
),
), ),
], ],
), ),
@ -170,28 +187,36 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
), ),
// Pinned action keys // Pinned action keys
Padding( SlideFadeEntrance(
padding: EdgeInsets.all(padding), delay: const Duration(milliseconds: 340),
child: ElevatedButton( child: Padding(
onPressed: () { padding: EdgeInsets.all(padding),
Navigator.push( child: InteractiveScale(
context, child: ElevatedButton(
FadeSlidePageRoute( onPressed: () {
child: SetupCompleteScreen( Navigator.push(
businessName: widget.businessName, context,
businessType: widget.businessType, FadeSlidePageRoute(
gstin: widget.gstin, child: SetupCompleteScreen(
barcodeScanner: _barcode, businessName: widget.businessName,
kotManagement: _kot, businessType: widget.businessType,
inventoryAlerts: _inventory, gstin: widget.gstin,
barcodeScanner: _barcode,
kotManagement: _kot,
inventoryAlerts: _inventory,
),
),
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
), ),
), ),
); child: const Text('Save & Continue'),
}, ),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
), ),
child: const Text('Save & Continue'),
), ),
) )
], ],
@ -210,64 +235,67 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
ThemeData theme, ThemeData theme,
) { ) {
return Container( return InteractiveScale(
padding: const EdgeInsets.all(16), onTap: () => onChanged(!value),
decoration: BoxDecoration( child: Container(
color: Colors.white, padding: const EdgeInsets.all(16),
borderRadius: BorderRadius.circular(16), decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200]!), color: Colors.white,
boxShadow: [ borderRadius: BorderRadius.circular(16),
BoxShadow( border: Border.all(color: Colors.grey[200]!),
color: Colors.black.withOpacity(0.02), boxShadow: [
blurRadius: 10, BoxShadow(
offset: const Offset(0, 4), color: Colors.black.withOpacity(0.02),
) blurRadius: 10,
], offset: const Offset(0, 4),
), )
child: Row( ],
crossAxisAlignment: CrossAxisAlignment.center, ),
children: [ child: Row(
Container( crossAxisAlignment: CrossAxisAlignment.center,
width: 44, children: [
height: 44, Container(
decoration: BoxDecoration( width: 44,
color: Colors.grey[100], height: 44,
shape: BoxShape.circle, decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.grey[600], size: 22),
), ),
child: Icon(icon, color: Colors.grey[600], size: 22), const SizedBox(width: 14),
), Expanded(
const SizedBox(width: 14), child: Column(
Expanded( crossAxisAlignment: CrossAxisAlignment.start,
child: Column( mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, children: [
mainAxisSize: MainAxisSize.min, Text(
children: [ title,
Text( style: const TextStyle(
title, fontSize: 15,
style: const TextStyle( fontWeight: FontWeight.bold,
fontSize: 15, color: Colors.black87,
fontWeight: FontWeight.bold, ),
color: Colors.black87,
), ),
), const SizedBox(height: 4),
const SizedBox(height: 4), Text(
Text( subtitle,
subtitle, style: TextStyle(
style: TextStyle( fontSize: 11,
fontSize: 11, color: Colors.grey[500],
color: Colors.grey[500], ),
), )
) ],
], ),
), ),
), Switch(
Switch( value: value,
value: value, onChanged: onChanged,
onChanged: onChanged, activeColor: Colors.white,
activeColor: Colors.white, activeTrackColor: theme.colorScheme.primary,
activeTrackColor: theme.colorScheme.primary, ),
), ],
], ),
), ),
); );
} }

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; 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:contacts_service/contacts_service.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import '../theme.dart'; import '../theme.dart';
import '../route_transitions.dart'; import '../route_transitions.dart';
import 'otp_verification_screen.dart'; import 'otp_verification_screen.dart';
@ -16,8 +16,8 @@ class MockContact {
} }
class MockPhone { class MockPhone {
final String? value; final String number;
MockPhone(this.value); MockPhone(this.number);
} }
class MobileLoginScreen extends StatefulWidget { class MobileLoginScreen extends StatefulWidget {
@ -103,7 +103,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
]; ];
} else { } else {
try { try {
final List<Contact> list = await ContactsService.getContacts(withThumbnails: false); final List<Contact> list = await FlutterContacts.getContacts(withProperties: true, withPhoto: false);
return list; return list;
} catch (e) { } catch (e) {
return []; return [];
@ -166,7 +166,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
setDialogState(() { setDialogState(() {
filteredContacts = allContacts.where((contact) { filteredContacts = allContacts.where((contact) {
final name = (contact.displayName ?? '').toLowerCase(); final name = (contact.displayName ?? '').toLowerCase();
final phone = (contact.phones.isNotEmpty ? contact.phones.first.value ?? '' : '').toLowerCase(); final phone = (contact.phones.isNotEmpty ? contact.phones.first.number : '').toLowerCase();
return name.contains(query.toLowerCase()) || phone.contains(query.toLowerCase()); return name.contains(query.toLowerCase()) || phone.contains(query.toLowerCase());
}).toList(); }).toList();
}); });
@ -183,7 +183,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final contact = filteredContacts[index]; final contact = filteredContacts[index];
final name = contact.displayName ?? 'Unknown'; final name = contact.displayName ?? 'Unknown';
final phone = contact.phones.isNotEmpty ? contact.phones.first.value : 'No number'; final phone = contact.phones.isNotEmpty ? contact.phones.first.number : 'No number';
return ListTile( return ListTile(
title: Text(name), title: Text(name),
subtitle: Text(phone), subtitle: Text(phone),
@ -256,318 +256,335 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( SlideFadeEntrance(
mainAxisAlignment: MainAxisAlignment.spaceBetween, delay: Duration.zero,
children: [
Text(
'Step 2 of 5',
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurfaceVariant,
),
),
const Text(
'Mobile Login',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFAF101A),
),
),
],
),
const SizedBox(height: 8),
Row(
children: List.generate(5, (index) {
return Expanded(
child: Container(
height: 6,
margin: EdgeInsets.only(
left: index == 0 ? 0 : 3.0,
right: index == 4 ? 0 : 3.0,
),
decoration: BoxDecoration(
color: index <= 1 ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
borderRadius: BorderRadius.circular(100),
),
),
);
}),
),
const SizedBox(height: 16),
Text(
'Welcome Back',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Enter your mobile number to sign in or create a new account.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFFAFFD1).withOpacity(0.8),
borderRadius: BorderRadius.circular(100),
border: Border.all(color: const Color(0xFF016619).withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock, size: 14, color: Color(0xFF016619)),
const SizedBox(width: 6),
Text(
'Secure OTP Verification',
style: theme.textTheme.labelSmall?.copyWith(
color: const Color(0xFF016619),
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF3F3F3),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Container( Text(
width: 40, 'Step 2 of 5',
height: 40, style: theme.textTheme.labelMedium?.copyWith(
decoration: const BoxDecoration( fontWeight: FontWeight.bold,
color: Color(0xFF005FAF), color: theme.colorScheme.onSurfaceVariant,
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified_user,
color: Colors.white,
size: 24,
), ),
), ),
const SizedBox(width: 12), const Text(
Expanded( 'Mobile Login',
child: Column( style: TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontFamily: 'Plus Jakarta Sans',
children: [ fontSize: 14,
Text( fontWeight: FontWeight.bold,
'Verify instantly with', color: Color(0xFFAF101A),
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
const Text(
'Truecaller',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
), ),
), ),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 8),
const Text( Row(
'+91 98765 43210', children: List.generate(5, (index) {
style: TextStyle( return Expanded(
fontFamily: 'Plus Jakarta Sans', child: Container(
fontSize: 24, height: 6,
fontWeight: FontWeight.bold, margin: EdgeInsets.only(
color: Colors.black, left: index == 0 ? 0 : 3.0,
), right: index == 4 ? 0 : 3.0,
), ),
const SizedBox(height: 16), decoration: BoxDecoration(
ElevatedButton.icon( color: index <= 1 ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
onPressed: () { borderRadius: BorderRadius.circular(100),
_controller.text = '9876543210';
setState(() {
_isValid = true;
});
showTopAlert(
context,
'Phone verified automatically with Truecaller! Joining PozoApp...',
icon: Icons.verified_user_outlined,
);
Navigator.push(
context,
FadeSlidePageRoute(
child: const OtpVerificationScreen(
mobileNumber: '9876543210',
), ),
), ),
); );
}, }),
icon: const Icon(Icons.flash_on, size: 18, color: Colors.white), ),
label: const Text( const SizedBox(height: 16),
'1-Tap Verification', Text(
style: TextStyle( 'Welcome Back',
fontFamily: 'Inter', style: theme.textTheme.headlineLarge?.copyWith(
fontSize: 16, fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Enter your mobile number to sign in or create a new account.',
style: theme.textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 100),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFFAFFD1).withOpacity(0.8),
borderRadius: BorderRadius.circular(100),
border: Border.all(color: const Color(0xFF016619).withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock, size: 14, color: Color(0xFF016619)),
const SizedBox(width: 6),
Text(
'Secure OTP Verification',
style: theme.textTheme.labelSmall?.copyWith(
color: const Color(0xFF016619),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
style: ElevatedButton.styleFrom( ],
backgroundColor: const Color(0xFF005FAF),
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 1,
),
),
const SizedBox(height: 12),
Text(
'No OTP required. Your phone will be verified automatically.',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
height: 1.4,
),
),
],
),
),
SizedBox(height: gap),
Row(
children: [
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Or enter mobile number manually',
style: theme.textTheme.labelMedium?.copyWith(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
), ),
const Expanded(child: Divider(color: Color(0xFFE4BEBA))), ),
], ),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 200),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF3F3F3),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
),
child: Column(
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFF005FAF),
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified_user,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Verify instantly with',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
const Text(
'Truecaller',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
],
),
const SizedBox(height: 20),
const Text(
'+91 98765 43210',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
_controller.text = '9876543210';
setState(() {
_isValid = true;
});
showTopAlert(
context,
'Phone verified automatically with Truecaller! Joining PozoApp...',
icon: Icons.verified_user_outlined,
);
Navigator.push(
context,
FadeSlidePageRoute(
child: const OtpVerificationScreen(
mobileNumber: '9876543210',
),
),
);
},
icon: const Icon(Icons.flash_on, size: 18, color: Colors.white),
label: const Text(
'1-Tap Verification',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF005FAF),
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 1,
),
),
const SizedBox(height: 12),
Text(
'No OTP required. Your phone will be verified automatically.',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
height: 1.4,
),
),
],
),
),
), ),
SizedBox(height: gap), SizedBox(height: gap),
Text( SlideFadeEntrance(
'Mobile Number', delay: const Duration(milliseconds: 300),
style: theme.textTheme.labelSmall?.copyWith( child: Column(
color: theme.colorScheme.onSurface, crossAxisAlignment: CrossAxisAlignment.start,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.outlineVariant),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: Row(
children: [ children: [
Container( Row(
padding: const EdgeInsets.symmetric(horizontal: 12), children: [
decoration: BoxDecoration( const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
border: Border( Padding(
right: BorderSide(color: theme.colorScheme.outlineVariant), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Or enter mobile number manually',
style: theme.textTheme.labelMedium?.copyWith(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
), ),
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
],
),
SizedBox(height: gap),
Text(
'Mobile Number',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.outlineVariant),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 22, padding: const EdgeInsets.symmetric(horizontal: 12),
height: 14,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!, width: 0.5), border: Border(
right: BorderSide(color: theme.colorScheme.outlineVariant),
),
), ),
child: Column( child: Row(
children: [ children: [
Container(height: 4, color: const Color(0xFFFF9933)),
Container( Container(
height: 4, width: 22,
color: Colors.white, height: 14,
child: Center( decoration: BoxDecoration(
child: Container( border: Border.all(color: Colors.grey[300]!, width: 0.5),
width: 2, ),
height: 2, child: Column(
decoration: const BoxDecoration( children: [
color: Color(0xFF000080), Container(height: 4, color: const Color(0xFFFF9933)),
shape: BoxShape.circle, Container(
height: 4,
color: Colors.white,
child: Center(
child: Container(
width: 2,
height: 2,
decoration: const BoxDecoration(
color: Color(0xFF000080),
shape: BoxShape.circle,
),
),
),
), ),
), Container(height: 4, color: const Color(0xFF128807)),
],
), ),
), ),
Container(height: 4, color: const Color(0xFF128807)), const SizedBox(width: 8),
Text(
'+91',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
)
], ],
), ),
), ),
const SizedBox(width: 8), Expanded(
Text( child: TextField(
'+91', controller: _controller,
style: theme.textTheme.bodyMedium?.copyWith( keyboardType: TextInputType.phone,
fontWeight: FontWeight.bold, inputFormatters: [
color: theme.colorScheme.onSurface, FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: const InputDecoration(
hintText: '00000 00000',
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
contentPadding: EdgeInsets.symmetric(horizontal: 16),
),
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
), ),
) ),
IconButton(
icon: const Icon(Icons.contact_phone, color: Color(0xFFAF101A)),
onPressed: _pickContact,
),
], ],
), ),
), ),
Expanded(
child: TextField(
controller: _controller,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: const InputDecoration(
hintText: '00000 00000',
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
contentPadding: EdgeInsets.symmetric(horizontal: 16),
),
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
),
IconButton(
icon: const Icon(Icons.contact_phone, color: Color(0xFFAF101A)),
onPressed: _pickContact,
),
], ],
), ),
), ),
@ -577,66 +594,75 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
), ),
), ),
Column( SlideFadeEntrance(
mainAxisSize: MainAxisSize.min, delay: const Duration(milliseconds: 400),
children: [ child: Column(
ElevatedButton( mainAxisSize: MainAxisSize.min,
onPressed: _isValid children: [
? () { InteractiveScale(
Navigator.push( hoverScale: _isValid ? 1.03 : 1.0,
context, tapScale: _isValid ? 0.96 : 1.0,
FadeSlidePageRoute( child: ElevatedButton(
child: OtpVerificationScreen( onPressed: _isValid
mobileNumber: _controller.text, ? () {
), Navigator.push(
), context,
); FadeSlidePageRoute(
} child: OtpVerificationScreen(
: null, mobileNumber: _controller.text,
style: ElevatedButton.styleFrom( ),
minimumSize: const Size.fromHeight(56), ),
backgroundColor: _isValid ? const Color(0xFFAF101A) : Colors.grey[200], );
foregroundColor: _isValid ? Colors.white : Colors.grey[400], }
elevation: _isValid ? 2 : 0, : null,
shape: RoundedRectangleBorder( style: ElevatedButton.styleFrom(
borderRadius: BorderRadius.circular(8), minimumSize: const Size.fromHeight(56),
), backgroundColor: _isValid ? const Color(0xFFAF101A) : Colors.grey[200],
), foregroundColor: _isValid ? Colors.white : Colors.grey[400],
child: const Text('Send OTP'), elevation: _isValid ? 2 : 0,
), shape: RoundedRectangleBorder(
SizedBox(height: gap / 2), borderRadius: BorderRadius.circular(16),
OutlinedButton(
onPressed: () {
showTopAlert(
context,
'Connecting with Pozo Support... Please wait.',
icon: Icons.support_agent_outlined,
);
},
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(56),
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.forum, color: Color(0xFFAF101A), size: 18),
const SizedBox(width: 8),
const Text(
'Help / Talk to Support',
style: TextStyle(
color: Color(0xFFAF101A),
fontWeight: FontWeight.bold,
fontSize: 16,
), ),
), ),
], child: const Text('Send OTP'),
),
), ),
), SizedBox(height: gap / 2),
], InteractiveScale(
child: OutlinedButton(
onPressed: () {
showTopAlert(
context,
'Connecting with Pozo Support... Please wait.',
icon: Icons.support_agent_outlined,
);
},
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(56),
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.forum, color: Color(0xFFAF101A), size: 18),
const SizedBox(width: 8),
const Text(
'Help / Talk to Support',
style: TextStyle(
color: Color(0xFFAF101A),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
),
],
),
), ),
], ],
), ),

View File

@ -123,184 +123,212 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Alert envelope pulse symbol // Alert envelope pulse symbol with SlideFadeEntrance
Center( SlideFadeEntrance(
child: Container( delay: Duration.zero,
width: 96, child: Center(
height: 96, child: Container(
decoration: BoxDecoration( width: 96,
color: theme.colorScheme.surfaceContainerHigh, height: 96,
shape: BoxShape.circle, decoration: BoxDecoration(
), color: theme.colorScheme.surfaceContainerHigh,
child: Center( shape: BoxShape.circle,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 10,
)
],
),
child: Icon(
Icons.sms,
color: theme.colorScheme.primary,
size: 38,
),
), ),
), child: Center(
), child: Container(
), width: 72,
const SizedBox(height: 16), height: 72,
Text( decoration: BoxDecoration(
'Verifying your number', color: Colors.white,
textAlign: TextAlign.center, borderRadius: BorderRadius.circular(16),
style: theme.textTheme.headlineMedium?.copyWith( boxShadow: [
fontWeight: FontWeight.bold, BoxShadow(
), color: Colors.black.withOpacity(0.06),
), blurRadius: 10,
const SizedBox(height: 16), )
Row( ],
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Sent code to $displayMobile",
style: theme.textTheme.bodyMedium,
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Text(
'Edit',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
],
),
const SizedBox(height: 16),
// 6 Custom Inputs Fields
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(11, (index) {
if (index.isOdd) {
return const SizedBox(width: 8);
}
final fieldIndex = index ~/ 2;
return SizedBox(
width: 44,
child: TextField(
controller: _controllers[fieldIndex],
focusNode: _focusNodes[fieldIndex],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
decoration: InputDecoration(
counterText: '',
contentPadding: const EdgeInsets.symmetric(vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: theme.colorScheme.outlineVariant),
), ),
focusedBorder: OutlineInputBorder( child: Icon(
borderRadius: BorderRadius.circular(8), Icons.sms,
borderSide: BorderSide(color: theme.colorScheme.primary, width: 2), color: theme.colorScheme.primary,
size: 38,
), ),
), ),
onChanged: (value) {
if (value.isNotEmpty && fieldIndex < 5) {
_focusNodes[fieldIndex + 1].requestFocus();
}
},
), ),
); ),
}),
),
const SizedBox(height: 16),
// Auto detect loader
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
), ),
),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 100),
child: Text(
'Verifying your number',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
SlideFadeEntrance(
delay: const Duration(milliseconds: 150),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary),
),
),
const SizedBox(width: 12),
Text( Text(
'Auto-detecting OTP...', "Sent code to $displayMobile",
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodyMedium,
fontWeight: FontWeight.bold, ),
const SizedBox(width: 8),
InteractiveScale(
onTap: () => Navigator.pop(context),
child: Text(
'Edit',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
), ),
), ),
], ],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 20),
// Resending timer action column // 6 Custom Inputs Fields with SlideFadeEntrance
Row( SlideFadeEntrance(
mainAxisAlignment: MainAxisAlignment.center, delay: const Duration(milliseconds: 200),
children: [ child: Row(
const Icon(Icons.schedule, size: 16, color: Colors.grey), mainAxisAlignment: MainAxisAlignment.center,
const SizedBox(width: 6), children: List.generate(11, (index) {
Text( if (index.isOdd) {
_timeLeft > 0 return const SizedBox(width: 8);
? 'Resend code in ${_getFormattedTime()}' }
: 'Resend Code via SMS', final fieldIndex = index ~/ 2;
style: theme.textTheme.bodyMedium, return SizedBox(
width: 44,
child: TextField(
controller: _controllers[fieldIndex],
focusNode: _focusNodes[fieldIndex],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
decoration: InputDecoration(
counterText: '',
contentPadding: const EdgeInsets.symmetric(vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: theme.colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: theme.colorScheme.primary, width: 2),
),
),
onChanged: (value) {
if (value.isNotEmpty && fieldIndex < 5) {
_focusNodes[fieldIndex + 1].requestFocus();
}
},
),
);
}),
),
),
const SizedBox(height: 24),
// Auto detect loader card with SlideFadeEntrance
SlideFadeEntrance(
delay: const Duration(milliseconds: 250),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
), ),
], child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary),
),
),
const SizedBox(width: 12),
Text(
'Auto-detecting OTP...',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(height: 24),
// Resending timer action row
SlideFadeEntrance(
delay: const Duration(milliseconds: 300),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.schedule, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text(
_timeLeft > 0
? 'Resend code in ${_getFormattedTime()}'
: 'Resend Code via SMS',
style: theme.textTheme.bodyMedium,
),
],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// WhatsApp custom CTA button // WhatsApp custom CTA button with InteractiveScale
ElevatedButton( SlideFadeEntrance(
onPressed: () {}, delay: const Duration(milliseconds: 350),
style: ElevatedButton.styleFrom( child: InteractiveScale(
backgroundColor: Colors.white, child: OutlinedButton.icon(
foregroundColor: Colors.black87, onPressed: () {
side: const BorderSide(color: Color(0xFFE2E2E2)), showTopAlert(
elevation: 1, context,
minimumSize: const Size(200, 48), 'WhatsApp verification request sent!',
), icon: Icons.chat_bubble_outline,
child: Row( );
mainAxisSize: MainAxisSize.min, },
children: const [ icon: const Icon(Icons.forum, color: Colors.green, size: 18),
Text( label: const Text(
'Resend via WhatsApp', 'Resend via WhatsApp',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
), ),
], style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Color(0xFFE2E2E2)),
elevation: 1,
minimumSize: const Size(220, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
), ),
) )
], ],
@ -326,28 +354,42 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildFooterAction(Icons.help_outline, 'Help'), _buildFooterAction(Icons.help_outline, 'Help', () {
_buildFooterAction(Icons.save, 'Save Draft'), showTopAlert(
TextButton.icon( context,
onPressed: () { 'Support agent contacted. We will call you shortly.',
Navigator.push( icon: Icons.support_agent_outlined,
context, );
FadeSlidePageRoute( }),
child: const BusinessSetupScreen(), _buildFooterAction(Icons.save, 'Save Draft', () {
showTopAlert(
context,
'Onboarding draft saved successfully!',
icon: Icons.save_outlined,
);
}),
InteractiveScale(
child: TextButton.icon(
onPressed: () {
Navigator.push(
context,
FadeSlidePageRoute(
child: const BusinessSetupScreen(),
),
);
},
icon: const Text(
'Verify Manually',
style: TextStyle(fontWeight: FontWeight.bold),
),
label: const Icon(Icons.arrow_forward, size: 16),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: theme.colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
), ),
);
},
icon: const Text(
'Verify Manually',
style: TextStyle(fontWeight: FontWeight.bold),
),
label: const Icon(Icons.arrow_forward, size: 16),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: theme.colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
), ),
), ),
) )
@ -363,21 +405,24 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
); );
} }
Widget _buildFooterAction(IconData icon, String label) { Widget _buildFooterAction(IconData icon, String label, VoidCallback onTap) {
return Column( return InteractiveScale(
mainAxisAlignment: MainAxisAlignment.center, onTap: onTap,
children: [ child: Column(
Icon(icon, color: Colors.grey[600], size: 22), mainAxisAlignment: MainAxisAlignment.center,
const SizedBox(height: 4), children: [
Text( Icon(icon, color: Colors.grey[600], size: 22),
label, const SizedBox(height: 4),
style: TextStyle( Text(
fontSize: 10, label,
fontWeight: FontWeight.bold, style: TextStyle(
color: Colors.grey[600], fontSize: 10,
), fontWeight: FontWeight.bold,
) color: Colors.grey[600],
], ),
)
],
),
); );
} }
} }

View File

@ -32,7 +32,6 @@ class _SetupCompleteScreenState extends State<SetupCompleteScreen> with SingleTi
late Animation<double> _cardOpacityAnimation; late Animation<double> _cardOpacityAnimation;
late Animation<double> _btnSlideAnimation; late Animation<double> _btnSlideAnimation;
late Animation<double> _btnOpacityAnimation; late Animation<double> _btnOpacityAnimation;
double _btnScale = 1.0;
@override @override
void initState() { void initState() {
@ -382,53 +381,42 @@ class _SetupCompleteScreenState extends State<SetupCompleteScreen> with SingleTi
), ),
); );
}, },
child: MouseRegion( child: InteractiveScale(
onEnter: (_) => setState(() => _btnScale = 1.03), child: ElevatedButton(
onExit: (_) => setState(() => _btnScale = 1.0), onPressed: () {
child: AnimatedScale( showTopAlert(
scale: _btnScale, context,
duration: const Duration(milliseconds: 150), 'Launching Register Dashboard... Enjoy billing!',
child: GestureDetector( icon: Icons.rocket_launch_outlined,
onTapDown: (_) => setState(() => _btnScale = 0.96), );
onTapUp: (_) => setState(() => _btnScale = 1.03), Navigator.push(
onTapCancel: () => setState(() => _btnScale = 1.0), context,
child: ElevatedButton( FadeSlidePageRoute(
onPressed: () { child: BillingDashboardScreen(
showTopAlert( businessName: widget.businessName,
context, businessType: widget.businessType,
'Launching Register Dashboard... Enjoy billing!', gstin: widget.gstin,
icon: Icons.rocket_launch_outlined, barcodeScanner: widget.barcodeScanner,
); kotManagement: widget.kotManagement,
Navigator.push( inventoryAlerts: widget.inventoryAlerts,
context,
FadeSlidePageRoute(
child: BillingDashboardScreen(
businessName: widget.businessName,
businessType: widget.businessType,
gstin: widget.gstin,
barcodeScanner: widget.barcodeScanner,
kotManagement: widget.kotManagement,
inventoryAlerts: widget.inventoryAlerts,
),
),
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
), ),
), ),
child: Row( );
mainAxisAlignment: MainAxisAlignment.center, },
children: const [ style: ElevatedButton.styleFrom(
Icon(Icons.receipt, size: 20), minimumSize: const Size.fromHeight(56),
SizedBox(width: 8), shape: RoundedRectangleBorder(
Text('Create Your First Bill'), borderRadius: BorderRadius.circular(16),
],
),
), ),
), ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.receipt, size: 20),
SizedBox(width: 8),
Text('Create Your First Bill'),
],
),
), ),
), ),
), ),

View File

@ -17,7 +17,6 @@ class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProvider
late Animation<double> _splashContainerOpacityAnimation; late Animation<double> _splashContainerOpacityAnimation;
late Animation<double> _contentFadeAnimation; late Animation<double> _contentFadeAnimation;
late Animation<Offset> _contentSlideAnimation; late Animation<Offset> _contentSlideAnimation;
double _btnScale = 1.0;
@override @override
void initState() { void initState() {
@ -491,37 +490,25 @@ class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProvider
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: context.isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start, crossAxisAlignment: context.isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [ children: [
MouseRegion( InteractiveScale(
onEnter: (_) => setState(() => _btnScale = 1.03), child: ElevatedButton(
onExit: (_) => setState(() => _btnScale = 1.0), onPressed: () {
child: GestureDetector( Navigator.push(
onTapDown: (_) => setState(() => _btnScale = 0.95), context,
onTapUp: (_) => setState(() => _btnScale = 1.03), FadeSlidePageRoute(child: const MobileLoginScreen()),
onTapCancel: () => setState(() => _btnScale = 1.0), );
child: AnimatedScale( },
scale: _btnScale, style: ElevatedButton.styleFrom(
duration: const Duration(milliseconds: 150), minimumSize: const Size(260, 54),
curve: Curves.easeOut, ),
child: ElevatedButton( child: Row(
onPressed: () { mainAxisAlignment: MainAxisAlignment.center,
Navigator.push( mainAxisSize: MainAxisSize.min,
context, children: const [
FadeSlidePageRoute(child: const MobileLoginScreen()), Text('Get Started'),
); SizedBox(width: 8),
}, Icon(Icons.arrow_forward, size: 18),
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),
],
),
),
), ),
), ),
), ),

View File

@ -122,7 +122,7 @@ class PozoTheme {
minimumSize: const Size.fromHeight(54), minimumSize: const Size.fromHeight(54),
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(16),
), ),
textStyle: GoogleFonts.plusJakartaSans( textStyle: GoogleFonts.plusJakartaSans(
fontSize: 16, fontSize: 16,

View File

@ -41,14 +41,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" 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: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -126,6 +118,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_contacts:
dependency: "direct main"
description:
name: flutter_contacts
sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
url: "https://pub.dev"
source: hosted
version: "1.1.9+2"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -424,14 +424,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
quiver:
dependency: transitive
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@ -12,7 +12,7 @@ dependencies:
google_fonts: ^5.1.0 google_fonts: ^5.1.0
image_picker: ^1.0.4 image_picker: ^1.0.4
permission_handler: ^11.0.1 permission_handler: ^11.0.1
contacts_service: ^0.6.3 flutter_contacts: ^1.1.9
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: