Replace SnackBars with custom iOS-style top alerts with icons

This commit is contained in:
Sridhar R 2026-06-10 14:05:58 +05:30
parent 4acfdb9c4a
commit 67142977c0
5 changed files with 198 additions and 69 deletions

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FadeSlidePageRoute<T> extends PageRouteBuilder<T> { class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
@ -34,3 +35,154 @@ class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
reverseTransitionDuration: const Duration(milliseconds: 200), reverseTransitionDuration: const Duration(milliseconds: 200),
); );
} }
void showTopAlert(
BuildContext context,
String message, {
bool isError = false,
IconData? icon,
}) {
final overlayState = Overlay.of(context);
late OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
builder: (context) => _TopAlertWidget(
message: message,
isError: isError,
icon: icon,
onDismiss: () {
overlayEntry.remove();
},
),
);
overlayState.insert(overlayEntry);
}
class _TopAlertWidget extends StatefulWidget {
final String message;
final bool isError;
final IconData? icon;
final VoidCallback onDismiss;
const _TopAlertWidget({
required this.message,
required this.isError,
this.icon,
required this.onDismiss,
});
@override
State<_TopAlertWidget> createState() => _TopAlertWidgetState();
}
class _TopAlertWidgetState extends State<_TopAlertWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _slideAnimation;
late Animation<double> _opacityAnimation;
Timer? _dismissTimer;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_slideAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
);
_opacityAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
_controller.forward();
_dismissTimer = Timer(const Duration(milliseconds: 2800), () {
if (mounted) {
_controller.reverse().then((_) {
widget.onDismiss();
});
}
});
}
@override
void dispose() {
_dismissTimer?.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final topPadding = mediaQuery.padding.top + 16.0;
IconData alertIcon = widget.icon ??
(widget.isError ? Icons.error_outline : Icons.check_circle_outline);
Color backgroundColor = widget.isError ? const Color(0xFFBA1A20) : const Color(0xFF2E7D32);
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Positioned(
top: topPadding * _slideAnimation.value - 80.0 * (1.0 - _slideAnimation.value),
left: 16.0,
right: 16.0,
child: Opacity(
opacity: _opacityAnimation.value,
child: Material(
color: Colors.transparent,
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 450),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
alertIcon,
color: Colors.white,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.message,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
),
),
),
);
},
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../route_transitions.dart';
class ProductItem { class ProductItem {
final String id; final String id;
@ -160,13 +161,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
} }
void _showErrorSnackBar(String message) { void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(context, message, isError: true);
SnackBar(
content: Text(message),
backgroundColor: Colors.red[800],
behavior: SnackBarBehavior.floating,
),
);
} }
double get _subtotal { double get _subtotal {
@ -944,12 +939,10 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
SnackBar( context,
content: Text('🧾 Thermal Printed: $_invoiceNumber successfully!'), 'Thermal Printed: $_invoiceNumber successfully!',
backgroundColor: const Color(0xFF10B981), icon: Icons.print,
behavior: SnackBarBehavior.floating,
),
); );
}, },
icon: const Icon(Icons.print, size: 16), icon: const Icon(Icons.print, size: 16),

View File

@ -90,29 +90,25 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
setState(() { setState(() {
_uploadedDocs[docName] = file.name; _uploadedDocs[docName] = file.name;
}); });
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
SnackBar( context,
content: Text('✅ Successfully loaded $docName!'), 'Successfully loaded $docName!',
backgroundColor: Colors.green, icon: Icons.check_circle_outline,
behavior: SnackBarBehavior.floating,
),
); );
} }
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
SnackBar( context,
content: Text('Failed to pick image: $e'), 'Failed to pick image: $e',
behavior: SnackBarBehavior.floating, isError: true,
),
); );
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('Permission denied. Cannot load document image.'), 'Permission denied. Cannot load document image.',
behavior: SnackBarBehavior.floating, isError: true,
),
); );
} }
} }
@ -298,11 +294,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
setState(() { setState(() {
_uploadedDocs.remove(docName); _uploadedDocs.remove(docName);
}); });
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
SnackBar( context,
content: Text('$docName attachment removed.'), '$docName attachment removed.',
behavior: SnackBarBehavior.floating, icon: Icons.delete_outline,
),
); );
}, },
), ),
@ -590,12 +585,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('GSTIN Verified Successfully!'), 'GSTIN Verified Successfully!',
backgroundColor: Colors.green, icon: Icons.verified_outlined,
behavior: SnackBarBehavior.floating,
),
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -628,12 +621,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
InkWell( InkWell(
onTap: () { onTap: () {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('📍 Location detected: Sector 62, Noida, UP'), 'Location detected: Sector 62, Noida, UP',
backgroundColor: Colors.green, icon: Icons.my_location_outlined,
behavior: SnackBarBehavior.floating,
),
); );
}, },
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),

View File

@ -84,11 +84,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
if (status.isGranted || kIsWeb) { if (status.isGranted || kIsWeb) {
_showContactPicker(); _showContactPicker();
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('Contacts permission was denied. Please enter the number manually or enable it in Settings.'), 'Contacts permission was denied. Please enter the number manually or enable it in Settings.',
behavior: SnackBarBehavior.floating, isError: true,
),
); );
} }
} }
@ -402,12 +401,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
setState(() { setState(() {
_isValid = true; _isValid = true;
}); });
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('⚡ Phone verified automatically with Truecaller! Joining PozoApp...'), 'Phone verified automatically with Truecaller! Joining PozoApp...',
backgroundColor: Color(0xFF005FAF), icon: Icons.verified_user_outlined,
behavior: SnackBarBehavior.floating,
),
); );
Navigator.push( Navigator.push(
context, context,
@ -610,11 +607,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
SizedBox(height: gap / 2), SizedBox(height: gap / 2),
OutlinedButton( OutlinedButton(
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('💬 Connecting with Pozo Support... Please wait.'), 'Connecting with Pozo Support... Please wait.',
behavior: SnackBarBehavior.floating, icon: Icons.support_agent_outlined,
),
); );
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(

View File

@ -293,13 +293,10 @@ class SetupCompleteScreen extends StatelessWidget {
// Primary Launcher CTA // Primary Launcher CTA
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( showTopAlert(
const SnackBar( context,
content: Text('⚡ Launching Register Dashboard... Enjoy billing!'), 'Launching Register Dashboard... Enjoy billing!',
behavior: SnackBarBehavior.floating, icon: Icons.rocket_launch_outlined,
backgroundColor: Colors.green,
duration: Duration(milliseconds: 1500),
),
); );
Navigator.push( Navigator.push(
context, context,