refactor: suppress error snackbars on session expiration, add tertiary theme color, and update subscription ticket UI styling.

This commit is contained in:
Suherdy Yacob 2026-06-15 18:51:07 +07:00
parent 4df528272e
commit f1a08d6396
8 changed files with 139 additions and 27 deletions

View File

@ -83,6 +83,11 @@ class _BranchesScreenState extends State<BranchesScreen> {
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
final errStr = e.toString().toLowerCase();
final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
errStr.contains('session expired') ||
errStr.contains('session_expired');
if (!isSessionExpired) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Error loading branches. Check connection.'),
@ -91,6 +96,7 @@ class _BranchesScreenState extends State<BranchesScreen> {
}
}
}
}
/// Haversine formula returns distance in kilometres.
double _distanceTo(Position pos, dynamic branch) {

View File

@ -52,9 +52,15 @@ class _LoyaltyDashboardState extends State<LoyaltyDashboard> {
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error loading data: $e')));
final errStr = e.toString().toLowerCase();
final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
errStr.contains('session expired') ||
errStr.contains('session_expired');
if (!isSessionExpired) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to load dashboard. Please try again.')),
);
}
}
}
}

View File

@ -55,12 +55,18 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
final errStr = e.toString().toLowerCase();
final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
errStr.contains('session expired') ||
errStr.contains('session_expired');
if (!isSessionExpired) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading notifications: $e')),
const SnackBar(content: Text('Failed to load notifications. Please try again.')),
);
}
}
}
}
@override
Widget build(BuildContext context) {

View File

@ -33,9 +33,15 @@ class _OrdersScreenState extends State<OrdersScreen> {
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error loading history: $e')));
final errStr = e.toString().toLowerCase();
final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
errStr.contains('session expired') ||
errStr.contains('session_expired');
if (!isSessionExpired) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to load order history. Please try again.')),
);
}
}
}
}

View File

@ -154,6 +154,7 @@ class OdooService {
'brand_logo': (res['brand_logo'] as String?) ?? '',
'primary_color': (res['primary_color'] as String?) ?? '#C62828',
'secondary_color': (res['secondary_color'] as String?) ?? '#FF8F00',
'tertiary_color': (res['tertiary_color'] as String?) ?? '#4A7C59',
'background_color': (res['background_color'] as String?) ?? '#FAF6EE',
'background_gradient_color': (res['background_gradient_color'] as String?) ?? '#F3EAD3',
};
@ -161,6 +162,7 @@ class OdooService {
await ThemeManager.instance.updateConfig(
primaryHex: configMap['primary_color']!,
secondaryHex: configMap['secondary_color']!,
tertiaryHex: configMap['tertiary_color']!,
backgroundHex: configMap['background_color']!,
backgroundGradientHex: configMap['background_gradient_color']!,
brandLogoB64: configMap['brand_logo']!,

View File

@ -11,12 +11,14 @@ class ThemeManager extends ChangeNotifier {
Color _primaryColor = AppTheme.primary;
Color _secondaryColor = AppTheme.secondary;
Color _tertiaryColor = AppTheme.tertiary;
Color _backgroundColor = AppTheme.surface;
Color _backgroundGradientColor = const Color(0xFFF3EAD3);
String _brandLogo = '';
Color get primaryColor => _primaryColor;
Color get secondaryColor => _secondaryColor;
Color get tertiaryColor => _tertiaryColor;
Color get backgroundColor => _backgroundColor;
Color get backgroundGradientColor => _backgroundGradientColor;
String get brandLogo => _brandLogo;
@ -24,6 +26,7 @@ class ThemeManager extends ChangeNotifier {
ThemeData get themeData => AppTheme.getTheme(
primaryColor: _primaryColor,
secondaryColor: _secondaryColor,
tertiaryColor: _tertiaryColor,
backgroundColor: _backgroundColor,
);
@ -32,6 +35,7 @@ class ThemeManager extends ChangeNotifier {
final prefs = await SharedPreferences.getInstance();
final primHex = prefs.getString('theme_primary_color');
final secHex = prefs.getString('theme_secondary_color');
final terHex = prefs.getString('theme_tertiary_color');
final bgHex = prefs.getString('theme_background_color');
final bgGradHex = prefs.getString('theme_background_gradient_color');
_brandLogo = prefs.getString('theme_brand_logo') ?? '';
@ -42,6 +46,9 @@ class ThemeManager extends ChangeNotifier {
if (secHex != null) {
_secondaryColor = _parseHexColor(secHex) ?? AppTheme.secondary;
}
if (terHex != null) {
_tertiaryColor = _parseHexColor(terHex) ?? AppTheme.tertiary;
}
if (bgHex != null) {
_backgroundColor = _parseHexColor(bgHex) ?? AppTheme.surface;
}
@ -54,6 +61,7 @@ class ThemeManager extends ChangeNotifier {
Future<void> updateConfig({
required String primaryHex,
required String secondaryHex,
required String tertiaryHex,
required String backgroundHex,
required String backgroundGradientHex,
required String brandLogoB64,
@ -61,12 +69,14 @@ class ThemeManager extends ChangeNotifier {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme_primary_color', primaryHex);
await prefs.setString('theme_secondary_color', secondaryHex);
await prefs.setString('theme_tertiary_color', tertiaryHex);
await prefs.setString('theme_background_color', backgroundHex);
await prefs.setString('theme_background_gradient_color', backgroundGradientHex);
await prefs.setString('theme_brand_logo', brandLogoB64);
_primaryColor = _parseHexColor(primaryHex) ?? AppTheme.primary;
_secondaryColor = _parseHexColor(secondaryHex) ?? AppTheme.secondary;
_tertiaryColor = _parseHexColor(tertiaryHex) ?? AppTheme.tertiary;
_backgroundColor = _parseHexColor(backgroundHex) ?? AppTheme.surface;
_backgroundGradientColor = _parseHexColor(backgroundGradientHex) ?? const Color(0xFFF3EAD3);
_brandLogo = brandLogoB64;

View File

@ -7,8 +7,12 @@ class AppTheme {
static const Color primaryContainer = Color(0xFF8A1C14);
static const Color secondary = Color(0xFFB58428); // Warm Gold/Honey Amber
static const Color secondaryContainer = Color(0xFFF3DCA2);
static const Color tertiary = Color(0xFF4A7C59); // Warm Sage/Forest Green
static const Color tertiaryContainer = Color(0xFFCCE8D6); // Soft sage tint
static const Color onPrimaryContainer = Colors.white;
static const Color onSecondaryContainer = Color(0xFF5A3E00);
static const Color onTertiary = Colors.white;
static const Color onTertiaryContainer = Color(0xFF0D3320); // Deep forest text
// Warm Ivory & Earthy Surface Hierarchy
static const Color surface = Color(0xFFFAF6EE); // Warm paper ivory background
@ -25,15 +29,28 @@ class AppTheme {
static ThemeData get lightTheme => getTheme();
static ThemeData getTheme({Color? primaryColor, Color? secondaryColor, Color? backgroundColor}) {
static ThemeData getTheme({Color? primaryColor, Color? secondaryColor, Color? tertiaryColor, Color? backgroundColor}) {
final baseTheme = ThemeData.light();
final pColor = primaryColor ?? primary;
final sColor = secondaryColor ?? secondary;
final tColor = tertiaryColor ?? tertiary;
final bg = backgroundColor ?? surface;
// Dynamically compute readable contrast text colors
final onPrimaryColor = pColor.computeLuminance() > 0.5 ? Color(0xFF2E251B) : Colors.white;
final onSecondaryColor = sColor.computeLuminance() > 0.5 ? Color(0xFF2E251B) : Colors.white;
final onPrimaryColor = pColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white;
final onSecondaryColor = sColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white;
final onTertiaryColor = tColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white;
// Tertiary container: a desaturated/lightened tint of the tertiary color
final tContainerColor = Color.lerp(tColor, Colors.white, 0.75) ?? tertiaryContainer;
final onTertiaryContainerColor = tColor.computeLuminance() > 0.5
? const Color(0xFF2E251B)
: Color.lerp(tColor, Colors.black, 0.7) ?? onTertiaryContainer;
final isWhiteBg = bg.r > 0.98 && bg.g > 0.98 && bg.b > 0.98;
final lowestColor = isWhiteBg ? const Color(0xFFFCFAF7) : const Color(0xFFFCFAF6);
final lowColor = isWhiteBg ? const Color(0xFFF5F3ED) : const Color(0xFFF7F1E3);
final containerColor = isWhiteBg ? const Color(0xFFEDEAE1) : const Color(0xFFF2EAD8);
final highColor = isWhiteBg ? const Color(0xFFE2DDD2) : const Color(0xFFE5D5BA);
return ThemeData(
useMaterial3: true,
@ -41,13 +58,22 @@ class AppTheme {
colorScheme: ColorScheme.light(
primary: pColor,
primaryContainer: pColor,
onPrimary: onPrimaryColor,
secondary: sColor,
secondaryContainer: sColor.withValues(alpha: 0.15),
onSecondary: onSecondaryColor,
onSecondaryContainer: onSecondaryColor,
tertiary: tColor,
tertiaryContainer: tContainerColor,
onTertiary: onTertiaryColor,
onTertiaryContainer: onTertiaryContainerColor,
surface: bg,
surfaceContainerLowest: lowestColor,
surfaceContainerLow: lowColor,
surfaceContainer: containerColor,
surfaceContainerHigh: highColor,
onSurface: onSurface,
onSurfaceVariant: onSurfaceVariant,
onPrimary: onPrimaryColor,
error: const Color(0xFFB02500),
),
textTheme: baseTheme.textTheme.copyWith(

View File

@ -100,11 +100,16 @@ class _SubscriptionCard extends StatelessWidget {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: CustomPaint(
foregroundPainter: TicketBorderPainter(
color: colorScheme.outline.withValues(alpha: 0.15),
strokeWidth: 1.0,
),
child: PhysicalShape(
clipper: TicketClipper(),
color: colorScheme.surfaceContainerLowest,
elevation: 3,
shadowColor: Colors.black.withValues(alpha: 0.15),
color: colorScheme.surfaceContainerLow,
elevation: 2,
shadowColor: Colors.black.withValues(alpha: 0.08),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
@ -282,8 +287,9 @@ class _SubscriptionCard extends StatelessWidget {
),
),
),
),
);
}
}
}
/// Custom Clipper for a ticket-like appearance with side notches.
@ -345,3 +351,47 @@ class DashedLinePainter extends CustomPainter {
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
/// Painter to draw the outer outline of the ticket shape.
class TicketBorderPainter extends CustomPainter {
final Color color;
final double strokeWidth;
TicketBorderPainter({required this.color, this.strokeWidth = 1.0});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
final path = Path();
path.moveTo(0, 0);
path.lineTo(size.width, 0);
const double notchY = 49.0;
const double notchRadius = 8.0;
path.lineTo(size.width, notchY - notchRadius);
path.arcToPoint(
Offset(size.width, notchY + notchRadius),
radius: const Radius.circular(notchRadius),
clockwise: false,
);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.lineTo(0, notchY + notchRadius);
path.arcToPoint(
const Offset(0, notchY - notchRadius),
radius: const Radius.circular(notchRadius),
clockwise: false,
);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}