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,11 +83,17 @@ class _BranchesScreenState extends State<BranchesScreen> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar( final errStr = e.toString().toLowerCase();
const SnackBar( final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
content: Text('Error loading branches. Check connection.'), errStr.contains('session expired') ||
), errStr.contains('session_expired');
); if (!isSessionExpired) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Error loading branches. Check connection.'),
),
);
}
} }
} }
} }

View File

@ -52,9 +52,15 @@ class _LoyaltyDashboardState extends State<LoyaltyDashboard> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of( final errStr = e.toString().toLowerCase();
context, final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
).showSnackBar(SnackBar(content: Text('Error loading data: $e'))); 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,9 +55,15 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar( final errStr = e.toString().toLowerCase();
SnackBar(content: Text('Error loading notifications: $e')), 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 notifications. Please try again.')),
);
}
} }
} }
} }

View File

@ -33,9 +33,15 @@ class _OrdersScreenState extends State<OrdersScreen> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of( final errStr = e.toString().toLowerCase();
context, final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') ||
).showSnackBar(SnackBar(content: Text('Error loading history: $e'))); 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?) ?? '', 'brand_logo': (res['brand_logo'] as String?) ?? '',
'primary_color': (res['primary_color'] as String?) ?? '#C62828', 'primary_color': (res['primary_color'] as String?) ?? '#C62828',
'secondary_color': (res['secondary_color'] as String?) ?? '#FF8F00', '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_color': (res['background_color'] as String?) ?? '#FAF6EE',
'background_gradient_color': (res['background_gradient_color'] as String?) ?? '#F3EAD3', 'background_gradient_color': (res['background_gradient_color'] as String?) ?? '#F3EAD3',
}; };
@ -161,6 +162,7 @@ class OdooService {
await ThemeManager.instance.updateConfig( await ThemeManager.instance.updateConfig(
primaryHex: configMap['primary_color']!, primaryHex: configMap['primary_color']!,
secondaryHex: configMap['secondary_color']!, secondaryHex: configMap['secondary_color']!,
tertiaryHex: configMap['tertiary_color']!,
backgroundHex: configMap['background_color']!, backgroundHex: configMap['background_color']!,
backgroundGradientHex: configMap['background_gradient_color']!, backgroundGradientHex: configMap['background_gradient_color']!,
brandLogoB64: configMap['brand_logo']!, brandLogoB64: configMap['brand_logo']!,

View File

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

View File

@ -7,8 +7,12 @@ class AppTheme {
static const Color primaryContainer = Color(0xFF8A1C14); static const Color primaryContainer = Color(0xFF8A1C14);
static const Color secondary = Color(0xFFB58428); // Warm Gold/Honey Amber static const Color secondary = Color(0xFFB58428); // Warm Gold/Honey Amber
static const Color secondaryContainer = Color(0xFFF3DCA2); 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 onPrimaryContainer = Colors.white;
static const Color onSecondaryContainer = Color(0xFF5A3E00); 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 // Warm Ivory & Earthy Surface Hierarchy
static const Color surface = Color(0xFFFAF6EE); // Warm paper ivory background static const Color surface = Color(0xFFFAF6EE); // Warm paper ivory background
@ -25,15 +29,28 @@ class AppTheme {
static ThemeData get lightTheme => getTheme(); 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 baseTheme = ThemeData.light();
final pColor = primaryColor ?? primary; final pColor = primaryColor ?? primary;
final sColor = secondaryColor ?? secondary; final sColor = secondaryColor ?? secondary;
final tColor = tertiaryColor ?? tertiary;
final bg = backgroundColor ?? surface; final bg = backgroundColor ?? surface;
// Dynamically compute readable contrast text colors // Dynamically compute readable contrast text colors
final onPrimaryColor = pColor.computeLuminance() > 0.5 ? Color(0xFF2E251B) : Colors.white; final onPrimaryColor = pColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white;
final onSecondaryColor = sColor.computeLuminance() > 0.5 ? 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( return ThemeData(
useMaterial3: true, useMaterial3: true,
@ -41,13 +58,22 @@ class AppTheme {
colorScheme: ColorScheme.light( colorScheme: ColorScheme.light(
primary: pColor, primary: pColor,
primaryContainer: pColor, primaryContainer: pColor,
onPrimary: onPrimaryColor,
secondary: sColor, secondary: sColor,
secondaryContainer: sColor.withValues(alpha: 0.15), secondaryContainer: sColor.withValues(alpha: 0.15),
onSecondary: onSecondaryColor,
onSecondaryContainer: onSecondaryColor, onSecondaryContainer: onSecondaryColor,
tertiary: tColor,
tertiaryContainer: tContainerColor,
onTertiary: onTertiaryColor,
onTertiaryContainer: onTertiaryContainerColor,
surface: bg, surface: bg,
surfaceContainerLowest: lowestColor,
surfaceContainerLow: lowColor,
surfaceContainer: containerColor,
surfaceContainerHigh: highColor,
onSurface: onSurface, onSurface: onSurface,
onSurfaceVariant: onSurfaceVariant, onSurfaceVariant: onSurfaceVariant,
onPrimary: onPrimaryColor,
error: const Color(0xFFB02500), error: const Color(0xFFB02500),
), ),
textTheme: baseTheme.textTheme.copyWith( textTheme: baseTheme.textTheme.copyWith(

View File

@ -100,13 +100,18 @@ class _SubscriptionCard extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: PhysicalShape( child: CustomPaint(
clipper: TicketClipper(), foregroundPainter: TicketBorderPainter(
color: colorScheme.surfaceContainerLowest, color: colorScheme.outline.withValues(alpha: 0.15),
elevation: 3, strokeWidth: 1.0,
shadowColor: Colors.black.withValues(alpha: 0.15), ),
child: Container( child: PhysicalShape(
padding: const EdgeInsets.all(20), clipper: TicketClipper(),
color: colorScheme.surfaceContainerLow,
elevation: 2,
shadowColor: Colors.black.withValues(alpha: 0.08),
child: Container(
padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -282,8 +287,9 @@ class _SubscriptionCard extends StatelessWidget {
), ),
), ),
), ),
); ),
} );
}
} }
/// Custom Clipper for a ticket-like appearance with side notches. /// Custom Clipper for a ticket-like appearance with side notches.
@ -345,3 +351,47 @@ class DashedLinePainter extends CustomPainter {
@override @override
bool shouldRepaint(CustomPainter oldDelegate) => false; 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;
}