diff --git a/lib/app/app.dart b/lib/app/app.dart new file mode 100644 index 0000000..aaeb161 --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,17 @@ +import 'package:cb_prestige_qr/app/theme/app_theme.dart'; +import 'package:cb_prestige_qr/features/auth/presentation/views/login_view.dart'; +import 'package:flutter/material.dart'; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'CB Prestige Banking', + debugShowCheckedModeBanner: false, + theme: AppTheme.dark(), + home: const LoginView(), + ); + } +} diff --git a/lib/core/utils/MainShell.dart b/lib/app/navigation/main_shell.dart similarity index 70% rename from lib/core/utils/MainShell.dart rename to lib/app/navigation/main_shell.dart index 8927e6a..96ba34c 100644 --- a/lib/core/utils/MainShell.dart +++ b/lib/app/navigation/main_shell.dart @@ -1,18 +1,18 @@ import 'dart:async'; import 'dart:ui'; -import 'package:cb_prestige_qr/core/utils/ScanShell.dart'; import 'package:cb_prestige_qr/core/presentation/widgets/global_loading_overlay.dart'; -import 'package:cb_prestige_qr/core/widgets/CenterNavButton.dart'; -import 'package:cb_prestige_qr/core/widgets/NavItem.dart'; -import 'package:cb_prestige_qr/features/analysis/presentation/pages/analysis_page.dart'; -import 'package:cb_prestige_qr/features/history/presentation/pages/history_page.dart'; -import 'package:cb_prestige_qr/features/home/presentation/pages/home.dart'; -import 'package:cb_prestige_qr/features/settings/presentation/pages/settings_page.dart'; +import 'package:cb_prestige_qr/core/widgets/center_nav_button.dart'; +import 'package:cb_prestige_qr/core/widgets/nav_item.dart'; +import 'package:cb_prestige_qr/features/analysis/presentation/views/analysis_view.dart'; +import 'package:cb_prestige_qr/features/history/presentation/views/history_view.dart'; +import 'package:cb_prestige_qr/features/home/presentation/views/home_view.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/views/scan_flow.dart'; +import 'package:cb_prestige_qr/features/settings/presentation/views/settings_view.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final navIndexNotifierProvider = NotifierProvider( +final navIndexProvider = NotifierProvider( NavIndexNotifier.new, ); @@ -39,7 +39,9 @@ class _MainShellState extends ConsumerState { void _startLoading([Duration duration = const Duration(seconds: 1)]) { _timer?.cancel(); - if (!_isLoading) setState(() => _isLoading = true); + if (!_isLoading) { + setState(() => _isLoading = true); + } _timer = Timer(duration, () { if (!mounted) return; setState(() => _isLoading = false); @@ -49,8 +51,10 @@ class _MainShellState extends ConsumerState { @override void initState() { super.initState(); - final initialIndex = ref.read(navIndexNotifierProvider); - if (initialIndex != 0) _startLoading(); + final initialIndex = ref.read(navIndexProvider); + if (initialIndex != 0) { + _startLoading(); + } } @override @@ -61,21 +65,20 @@ class _MainShellState extends ConsumerState { @override Widget build(BuildContext context) { - final selectedIndex = ref.watch(navIndexNotifierProvider); + final selectedIndex = ref.watch(navIndexProvider); final theme = Theme.of(context); - ref.listen(navIndexNotifierProvider, (previous, next) { - if (previous == null || previous == next) return; - if (next == 0) return; + ref.listen(navIndexProvider, (previous, next) { + if (previous == null || previous == next || next == 0) return; _startLoading(); }); - final pages = [ - const HomeView(), - const AnalysisPage(), - const SizedBox(), - const HistoryPage(), - const SettingsPage(), + const pages = [ + HomeView(), + AnalysisView(), + SizedBox(), + HistoryView(), + SettingsView(), ]; return Stack( @@ -113,18 +116,16 @@ class _MainShellState extends ConsumerState { child: NavItem( icon: Icons.home_rounded, isActive: selectedIndex == 0, - onTap: () => ref - .read(navIndexNotifierProvider.notifier) - .setIndex(0), + onTap: () => + ref.read(navIndexProvider.notifier).setIndex(0), ), ), Expanded( child: NavItem( icon: Icons.analytics, isActive: selectedIndex == 1, - onTap: () => ref - .read(navIndexNotifierProvider.notifier) - .setIndex(1), + onTap: () => + ref.read(navIndexProvider.notifier).setIndex(1), ), ), Expanded( @@ -144,18 +145,16 @@ class _MainShellState extends ConsumerState { child: NavItem( icon: Icons.history_rounded, isActive: selectedIndex == 3, - onTap: () => ref - .read(navIndexNotifierProvider.notifier) - .setIndex(3), + onTap: () => + ref.read(navIndexProvider.notifier).setIndex(3), ), ), Expanded( child: NavItem( icon: Icons.person_rounded, isActive: selectedIndex == 4, - onTap: () => ref - .read(navIndexNotifierProvider.notifier) - .setIndex(4), + onTap: () => + ref.read(navIndexProvider.notifier).setIndex(4), ), ), ], diff --git a/lib/app/theme/app_theme.dart b/lib/app/theme/app_theme.dart new file mode 100644 index 0000000..06c974a --- /dev/null +++ b/lib/app/theme/app_theme.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + const AppTheme._(); + + static ThemeData dark() { + return ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xff896e4b), + brightness: Brightness.dark, + ), + scaffoldBackgroundColor: const Color(0xff25262b), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xff896e4b), + foregroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xff2a2b31), + labelStyle: const TextStyle(color: Colors.white70), + hintStyle: const TextStyle(color: Colors.white38), + prefixIconColor: Colors.white70, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide(color: Colors.white.withOpacity(0.06)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Color(0xff896e4b), width: 1.2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Colors.redAccent), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Colors.redAccent), + ), + ), + ); + } +} diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart new file mode 100644 index 0000000..231ba7a --- /dev/null +++ b/lib/core/network/api_client.dart @@ -0,0 +1,18 @@ +import 'package:cb_prestige_qr/config.dart'; +import 'package:dio/dio.dart'; + +class ApiClient { + static Dio create() { + return Dio( + BaseOptions( + baseUrl: AppConfig.apiBaseUrl, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ), + ); + } +} diff --git a/lib/core/presentation/widgets/global_loading_overlay.dart b/lib/core/presentation/widgets/global_loading_overlay.dart index e4f1385..9e1d975 100644 --- a/lib/core/presentation/widgets/global_loading_overlay.dart +++ b/lib/core/presentation/widgets/global_loading_overlay.dart @@ -2,10 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; class GlobalLoadingOverlay extends StatelessWidget { - const GlobalLoadingOverlay({ - super.key, - required this.isLoading, - }); + const GlobalLoadingOverlay({super.key, required this.isLoading}); final bool isLoading; @@ -22,12 +19,8 @@ class GlobalLoadingOverlay extends StatelessWidget { filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: const SizedBox.expand(), ), - ColoredBox( - color: Colors.black.withOpacity(0.55), - ), - const Center( - child: _AnimatedLoader(), - ), + ColoredBox(color: Colors.black.withOpacity(0.55)), + const Center(child: _AnimatedLoader()), ], ), ), @@ -59,30 +52,15 @@ class _AnimatedLoaderState extends State<_AnimatedLoader> /// Main fade (smooth breathing) _mainOpacity = TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: 0.0, end: 1.0), - weight: 50, - ), - TweenSequenceItem( - tween: Tween(begin: 1.0, end: 0.0), - weight: 50, - ), - ]).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ), - ); + TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 50), + TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.0), weight: 50), + ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + /// Glow fade (slightly offset feel) _glowOpacity = Tween( begin: 0.2, end: 0.5, - ).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); } @override diff --git a/lib/core/providers/core_providers.dart b/lib/core/providers/core_providers.dart new file mode 100644 index 0000000..d9feaaa --- /dev/null +++ b/lib/core/providers/core_providers.dart @@ -0,0 +1,15 @@ +import 'package:cb_prestige_qr/core/network/api_client.dart'; +import 'package:cb_prestige_qr/core/services/device_id_service.dart'; +import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; +import 'package:dio/dio.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final localStorageServiceProvider = Provider( + (ref) => LocalStorageService(), +); + +final deviceIdServiceProvider = Provider( + (ref) => DeviceIdService(), +); + +final dioProvider = Provider((ref) => ApiClient.create()); diff --git a/lib/core/services/device_id_service.dart b/lib/core/services/device_id_service.dart index 0fc4711..3244a86 100644 --- a/lib/core/services/device_id_service.dart +++ b/lib/core/services/device_id_service.dart @@ -1,16 +1,14 @@ -import 'package:flutter/services.dart'; - class DeviceIdService { - static const _channel = MethodChannel('cb_prestige_qr/device'); - Future getDeviceId() async { - // final deviceId = await _channel.invokeMethod('getDeviceId'); + // final deviceId = await const MethodChannel( + // 'cb_prestige_qr/device', + // ).invokeMethod('getDeviceId'); // if (deviceId == null || deviceId.trim().isEmpty) { // throw PlatformException( // code: 'device_id_unavailable', // message: 'Unable to retrieve device ID.', // ); // } - return "demo_device_id"; + return 'demo_device_id'; } } diff --git a/lib/core/storage/local_storage_service.dart b/lib/core/storage/local_storage_service.dart index 91c52ba..186c1e9 100644 --- a/lib/core/storage/local_storage_service.dart +++ b/lib/core/storage/local_storage_service.dart @@ -20,4 +20,9 @@ class LocalStorageService { final prefs = await SharedPreferences.getInstance(); return prefs.getString(key); } + + Future remove(String key) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(key); + } } diff --git a/lib/core/widgets/CenterNavButton.dart b/lib/core/widgets/center_nav_button.dart similarity index 98% rename from lib/core/widgets/CenterNavButton.dart rename to lib/core/widgets/center_nav_button.dart index 7ccdd47..6a82909 100644 --- a/lib/core/widgets/CenterNavButton.dart +++ b/lib/core/widgets/center_nav_button.dart @@ -1,19 +1,19 @@ -import 'package:flutter/material.dart'; - -class CenterNavButton extends StatelessWidget { - final bool isActive; - final VoidCallback onTap; - - const CenterNavButton({ - super.key, - required this.isActive, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - +import 'package:flutter/material.dart'; + +class CenterNavButton extends StatelessWidget { + const CenterNavButton({ + super.key, + required this.isActive, + required this.onTap, + }); + + final bool isActive; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(20), @@ -27,13 +27,13 @@ class CenterNavButton extends StatelessWidget { : Colors.transparent, borderRadius: BorderRadius.circular(20), ), - child: Center( - child: AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: isActive ? 1.15 : 1.0, - child: Container( - height: 48, - width: 48, + child: Center( + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: isActive ? 1.15 : 1.0, + child: Container( + height: 48, + width: 48, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( @@ -50,15 +50,15 @@ class CenterNavButton extends StatelessWidget { ), ], ), - child: const Icon( - Icons.qr_code_scanner, - color: Colors.white, - size: 24, - ), - ), - ), - ), - ), - ); - } + child: const Icon( + Icons.qr_code_scanner, + color: Colors.white, + size: 24, + ), + ), + ), + ), + ), + ); + } } diff --git a/lib/core/widgets/NavItem.dart b/lib/core/widgets/nav_item.dart similarity index 98% rename from lib/core/widgets/NavItem.dart rename to lib/core/widgets/nav_item.dart index 11378ca..69ab182 100644 --- a/lib/core/widgets/NavItem.dart +++ b/lib/core/widgets/nav_item.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; - -class NavItem extends StatelessWidget { - final IconData icon; - final bool isActive; - final VoidCallback onTap; +class NavItem extends StatelessWidget { const NavItem({ super.key, required this.icon, @@ -12,6 +8,10 @@ class NavItem extends StatelessWidget { required this.onTap, }); + final IconData icon; + final bool isActive; + final VoidCallback onTap; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -29,20 +29,20 @@ class NavItem extends StatelessWidget { : Colors.transparent, borderRadius: BorderRadius.circular(20), ), - child: Center( - child: AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: isActive ? 1.2 : 1.0, - child: Icon( - icon, - size: 26, - color: isActive - ? theme.colorScheme.primary - : theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ); - } + child: Center( + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: isActive ? 1.2 : 1.0, + child: Icon( + icon, + size: 26, + color: isActive + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + } } diff --git a/lib/features/analysis/presentation/manager/analysis_chart_metric.dart b/lib/features/analysis/presentation/viewmodels/analysis_chart_metric.dart similarity index 100% rename from lib/features/analysis/presentation/manager/analysis_chart_metric.dart rename to lib/features/analysis/presentation/viewmodels/analysis_chart_metric.dart diff --git a/lib/features/analysis/presentation/manager/analysis_range.dart b/lib/features/analysis/presentation/viewmodels/analysis_range.dart similarity index 100% rename from lib/features/analysis/presentation/manager/analysis_range.dart rename to lib/features/analysis/presentation/viewmodels/analysis_range.dart diff --git a/lib/features/analysis/presentation/manager/analysis_ui_state.dart b/lib/features/analysis/presentation/viewmodels/analysis_ui_state.dart similarity index 98% rename from lib/features/analysis/presentation/manager/analysis_ui_state.dart rename to lib/features/analysis/presentation/viewmodels/analysis_ui_state.dart index bed4e8e..b247842 100644 --- a/lib/features/analysis/presentation/manager/analysis_ui_state.dart +++ b/lib/features/analysis/presentation/viewmodels/analysis_ui_state.dart @@ -40,7 +40,7 @@ class AnalysisSummaryUiModel { @immutable class AnalysisUiState { - AnalysisUiState({ + const AnalysisUiState({ int? rangeDays, String? rangeLabel, AnalysisSummaryUiModel? summary, @@ -62,7 +62,6 @@ class AnalysisUiState { final String? rangeLabel; final AnalysisSummaryUiModel? summary; final List? series; - final double? averageScansPerDay; final double? averagePointsPerDay; final double? pointsPerScan; diff --git a/lib/features/analysis/presentation/manager/analysis_view_model.dart b/lib/features/analysis/presentation/viewmodels/analysis_view_model.dart similarity index 96% rename from lib/features/analysis/presentation/manager/analysis_view_model.dart rename to lib/features/analysis/presentation/viewmodels/analysis_view_model.dart index 6035b39..4129a56 100644 --- a/lib/features/analysis/presentation/manager/analysis_view_model.dart +++ b/lib/features/analysis/presentation/viewmodels/analysis_view_model.dart @@ -9,12 +9,12 @@ import 'analysis_chart_metric.dart'; import 'analysis_range.dart'; import 'analysis_ui_state.dart'; -final analysisRangeNotifierProvider = +final analysisRangeProvider = NotifierProvider( AnalysisRangeNotifier.new, ); -final analysisChartMetricNotifierProvider = +final analysisChartMetricProvider = NotifierProvider( AnalysisChartMetricNotifier.new, ); @@ -53,7 +53,7 @@ final analysisViewModelProvider = class AnalysisViewModel extends AsyncNotifier { @override Future build() async { - final rangePreset = ref.watch(analysisRangeNotifierProvider); + final rangePreset = ref.watch(analysisRangeProvider); final content = await ref.watch(_getAnalysisContentProvider)( rangeDays: rangePreset.days, ); diff --git a/lib/features/analysis/presentation/pages/analysis_page.dart b/lib/features/analysis/presentation/views/analysis_view.dart similarity index 97% rename from lib/features/analysis/presentation/pages/analysis_page.dart rename to lib/features/analysis/presentation/views/analysis_view.dart index c5b567a..1ae5d33 100644 --- a/lib/features/analysis/presentation/pages/analysis_page.dart +++ b/lib/features/analysis/presentation/views/analysis_view.dart @@ -1,20 +1,20 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../manager/analysis_chart_metric.dart'; -import '../manager/analysis_range.dart'; -import '../manager/analysis_ui_state.dart'; -import '../manager/analysis_view_model.dart'; +import '../viewmodels/analysis_chart_metric.dart'; +import '../viewmodels/analysis_range.dart'; +import '../viewmodels/analysis_ui_state.dart'; +import '../viewmodels/analysis_view_model.dart'; import '../widgets/analysis_line_chart.dart'; -class AnalysisPage extends ConsumerWidget { - const AnalysisPage({super.key}); +class AnalysisView extends ConsumerWidget { + const AnalysisView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final rangePreset = ref.watch(analysisRangeNotifierProvider); - final chartMetric = ref.watch(analysisChartMetricNotifierProvider); + final rangePreset = ref.watch(analysisRangeProvider); + final chartMetric = ref.watch(analysisChartMetricProvider); final stateAsync = ref.watch(analysisViewModelProvider); return Scaffold( @@ -34,9 +34,8 @@ class AnalysisPage extends ConsumerWidget { children: [ _RangeSelector( selected: rangePreset, - onSelected: (preset) => ref - .read(analysisRangeNotifierProvider.notifier) - .setRange(preset), + onSelected: (preset) => + ref.read(analysisRangeProvider.notifier).setRange(preset), ), const SizedBox(height: 12), stateAsync.when( @@ -44,7 +43,7 @@ class AnalysisPage extends ConsumerWidget { state: state, chartMetric: chartMetric, onMetricSelected: (metric) => ref - .read(analysisChartMetricNotifierProvider.notifier) + .read(analysisChartMetricProvider.notifier) .setMetric(metric), ), loading: () => const _LoadingBody(), diff --git a/lib/features/auth/data/data_sources/auth_local_data_source.dart b/lib/features/auth/data/data_sources/auth_local_data_source.dart new file mode 100644 index 0000000..dd05a17 --- /dev/null +++ b/lib/features/auth/data/data_sources/auth_local_data_source.dart @@ -0,0 +1,72 @@ +import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart'; + +abstract class AuthLocalDataSource { + Future saveSession(User user); + Future getSavedUserProfile(); + Future clearSession(); +} + +class AuthLocalDataSourceImpl implements AuthLocalDataSource { + AuthLocalDataSourceImpl(this._storage); + + final LocalStorageService _storage; + + static const _usernameKey = 'logged_in_username'; + static const _displayNameKey = 'logged_in_display_name'; + static const _roleKey = 'logged_in_role'; + static const _branchKey = 'logged_in_branch'; + static const _tokenKey = 'logged_in_token'; + + @override + Future saveSession(User user) async { + await _storage.setString(_usernameKey, user.username); + await _storage.setString(_displayNameKey, user.displayName); + await _storage.setString(_roleKey, user.roleLabel); + await _storage.setString(_branchKey, user.branchLabel); + await _storage.setString(_tokenKey, user.authToken); + } + + @override + Future getSavedUserProfile() async { + final username = await _storage.getString(_usernameKey); + if (username == null || username.trim().isEmpty) return null; + + return UserProfile( + username: username, + displayName: + await _storage.getString(_displayNameKey) ?? + _displayNameFromUsername(username), + roleLabel: await _storage.getString(_roleKey) ?? 'Cashier', + branchLabel: await _storage.getString(_branchKey) ?? 'Prestige Counter', + ); + } + + @override + Future clearSession() async { + await _storage.remove(_usernameKey); + await _storage.remove(_displayNameKey); + await _storage.remove(_roleKey); + await _storage.remove(_branchKey); + await _storage.remove(_tokenKey); + } + + static String _displayNameFromUsername(String username) { + final words = username + .replaceAll(RegExp(r'[._-]+'), ' ') + .trim() + .split(RegExp(r'\s+')) + .where((word) => word.isNotEmpty) + .toList(growable: false); + if (words.isEmpty) return 'Cashier'; + + return words.map(_capitalize).join(' '); + } + + static String _capitalize(String value) { + if (value.isEmpty) return value; + if (value.length == 1) return value.toUpperCase(); + return '${value.substring(0, 1).toUpperCase()}${value.substring(1)}'; + } +} diff --git a/lib/features/auth/data/data_sources/auth_remote_data_source.dart b/lib/features/auth/data/data_sources/auth_remote_data_source.dart new file mode 100644 index 0000000..8dd0561 --- /dev/null +++ b/lib/features/auth/data/data_sources/auth_remote_data_source.dart @@ -0,0 +1,48 @@ +import 'package:cb_prestige_qr/features/auth/data/models/login_request_model.dart'; +import 'package:cb_prestige_qr/features/auth/data/models/login_response_model.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user.dart'; + +abstract class AuthRemoteDataSource { + Future login(LoginRequestModel request); +} + +class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { + const AuthRemoteDataSourceImpl(); + + @override + Future login(LoginRequestModel request) async { + await Future.delayed(const Duration(milliseconds: 800)); + + final username = request.username.trim(); + if (username.isEmpty || request.password.trim().isEmpty) { + throw Exception('Username and password are required.'); + } + + final user = User( + username: username, + displayName: _displayNameFromUsername(username), + roleLabel: 'Cashier', + branchLabel: 'Prestige Counter', + authToken: 'demo-token-${request.deviceId}-$username', + ); + + return LoginResponseModel(user: user, token: user.authToken); + } + + static String _displayNameFromUsername(String username) { + final words = username + .replaceAll(RegExp(r'[._-]+'), ' ') + .trim() + .split(RegExp(r'\s+')) + .where((word) => word.isNotEmpty) + .toList(growable: false); + if (words.isEmpty) return 'Cashier'; + return words.map(_capitalize).join(' '); + } + + static String _capitalize(String value) { + if (value.isEmpty) return value; + if (value.length == 1) return value.toUpperCase(); + return '${value.substring(0, 1).toUpperCase()}${value.substring(1)}'; + } +} diff --git a/lib/features/auth/data/models/login_request_model.dart b/lib/features/auth/data/models/login_request_model.dart new file mode 100644 index 0000000..044a3fb --- /dev/null +++ b/lib/features/auth/data/models/login_request_model.dart @@ -0,0 +1,19 @@ +class LoginRequestModel { + const LoginRequestModel({ + required this.username, + required this.password, + required this.deviceId, + }); + + final String username; + final String password; + final String deviceId; + + Map toJson() { + return { + 'username': username, + 'password': password, + 'device_id': deviceId, + }; + } +} diff --git a/lib/features/auth/data/models/login_response_model.dart b/lib/features/auth/data/models/login_response_model.dart new file mode 100644 index 0000000..48fc1fa --- /dev/null +++ b/lib/features/auth/data/models/login_response_model.dart @@ -0,0 +1,8 @@ +import 'package:cb_prestige_qr/features/auth/domain/entities/user.dart'; + +class LoginResponseModel { + const LoginResponseModel({required this.user, required this.token}); + + final User user; + final String token; +} diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/lib/features/auth/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..e01a351 --- /dev/null +++ b/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -0,0 +1,50 @@ +import 'package:cb_prestige_qr/core/services/device_id_service.dart'; +import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_local_data_source.dart'; +import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_remote_data_source.dart'; +import 'package:cb_prestige_qr/features/auth/data/models/login_request_model.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart'; +import 'package:cb_prestige_qr/features/auth/domain/repositories/auth_repository.dart'; + +class AuthRepositoryImpl implements AuthRepository { + const AuthRepositoryImpl( + this._remoteDataSource, + this._localDataSource, + this._deviceIdService, + ); + + final AuthRemoteDataSource _remoteDataSource; + final AuthLocalDataSource _localDataSource; + final DeviceIdService _deviceIdService; + + @override + Future login({ + required String username, + required String password, + }) async { + final deviceId = await _deviceIdService.getDeviceId(); + final response = await _remoteDataSource.login( + LoginRequestModel( + username: username, + password: password, + deviceId: deviceId, + ), + ); + + await _localDataSource.saveSession(response.user); + + return UserProfile( + username: response.user.username, + displayName: response.user.displayName, + roleLabel: response.user.roleLabel, + branchLabel: response.user.branchLabel, + ); + } + + @override + Future getCurrentUserProfile() { + return _localDataSource.getSavedUserProfile(); + } + + @override + Future logout() => _localDataSource.clearSession(); +} diff --git a/lib/features/auth/data/user_profile_repository.dart b/lib/features/auth/data/user_profile_repository.dart deleted file mode 100644 index 524c144..0000000 --- a/lib/features/auth/data/user_profile_repository.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; -import 'package:cb_prestige_qr/features/auth/domain/user_profile.dart'; - -class UserProfileRepository { - UserProfileRepository(this._storage); - - final LocalStorageService _storage; - - static const _usernameKey = 'logged_in_username'; - static const _displayNameKey = 'logged_in_display_name'; - static const _roleKey = 'logged_in_role'; - static const _branchKey = 'logged_in_branch'; - - Future load() async { - final username = await _storage.getString(_usernameKey) ?? 'cashier'; - final displayName = - await _storage.getString(_displayNameKey) ?? - _displayNameFromUsername(username); - final roleLabel = await _storage.getString(_roleKey) ?? 'Cashier'; - final branchLabel = - await _storage.getString(_branchKey) ?? 'Prestige Counter'; - - return UserProfile( - username: username, - displayName: displayName, - roleLabel: roleLabel, - branchLabel: branchLabel, - ); - } - - Future saveFromUsername(String username) async { - final normalizedUsername = username.trim(); - final safeUsername = normalizedUsername.isEmpty - ? 'cashier' - : normalizedUsername; - - await _storage.setString(_usernameKey, safeUsername); - await _storage.setString( - _displayNameKey, - _displayNameFromUsername(safeUsername), - ); - await _storage.setString(_roleKey, 'Cashier'); - await _storage.setString(_branchKey, 'Prestige Counter'); - } - - static String _displayNameFromUsername(String username) { - final words = username - .replaceAll(RegExp(r'[._-]+'), ' ') - .trim() - .split(RegExp(r'\s+')) - .where((word) => word.isNotEmpty) - .toList(growable: false); - if (words.isEmpty) return 'Cashier'; - - return words.map(_capitalize).join(' '); - } - - static String _capitalize(String value) { - if (value.isEmpty) return value; - if (value.length == 1) return value.toUpperCase(); - return '${value.substring(0, 1).toUpperCase()}${value.substring(1)}'; - } -} diff --git a/lib/features/auth/di/auth_providers.dart b/lib/features/auth/di/auth_providers.dart new file mode 100644 index 0000000..5004e4f --- /dev/null +++ b/lib/features/auth/di/auth_providers.dart @@ -0,0 +1,38 @@ +import 'package:cb_prestige_qr/core/providers/core_providers.dart'; +import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_local_data_source.dart'; +import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_remote_data_source.dart'; +import 'package:cb_prestige_qr/features/auth/data/repositories/auth_repository_impl.dart'; +import 'package:cb_prestige_qr/features/auth/domain/repositories/auth_repository.dart'; +import 'package:cb_prestige_qr/features/auth/domain/use_cases/get_current_user_profile_use_case.dart'; +import 'package:cb_prestige_qr/features/auth/domain/use_cases/login_use_case.dart'; +import 'package:cb_prestige_qr/features/auth/domain/use_cases/logout_use_case.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final authLocalDataSourceProvider = Provider( + (ref) => AuthLocalDataSourceImpl(ref.watch(localStorageServiceProvider)), +); + +final authRemoteDataSourceProvider = Provider( + (ref) => const AuthRemoteDataSourceImpl(), +); + +final authRepositoryProvider = Provider( + (ref) => AuthRepositoryImpl( + ref.watch(authRemoteDataSourceProvider), + ref.watch(authLocalDataSourceProvider), + ref.watch(deviceIdServiceProvider), + ), +); + +final loginUseCaseProvider = Provider( + (ref) => LoginUseCase(ref.watch(authRepositoryProvider)), +); + +final getCurrentUserProfileUseCaseProvider = + Provider( + (ref) => GetCurrentUserProfileUseCase(ref.watch(authRepositoryProvider)), + ); + +final logoutUseCaseProvider = Provider( + (ref) => LogoutUseCase(ref.watch(authRepositoryProvider)), +); diff --git a/lib/features/auth/domain/entities/user.dart b/lib/features/auth/domain/entities/user.dart new file mode 100644 index 0000000..c663ff5 --- /dev/null +++ b/lib/features/auth/domain/entities/user.dart @@ -0,0 +1,15 @@ +class User { + const User({ + required this.username, + required this.displayName, + required this.roleLabel, + required this.branchLabel, + required this.authToken, + }); + + final String username; + final String displayName; + final String roleLabel; + final String branchLabel; + final String authToken; +} diff --git a/lib/features/auth/domain/user_profile.dart b/lib/features/auth/domain/entities/user_profile.dart similarity index 100% rename from lib/features/auth/domain/user_profile.dart rename to lib/features/auth/domain/entities/user_profile.dart diff --git a/lib/features/auth/domain/repositories/auth_repository.dart b/lib/features/auth/domain/repositories/auth_repository.dart new file mode 100644 index 0000000..66d148a --- /dev/null +++ b/lib/features/auth/domain/repositories/auth_repository.dart @@ -0,0 +1,12 @@ +import '../entities/user_profile.dart'; + +abstract class AuthRepository { + Future login({ + required String username, + required String password, + }); + + Future getCurrentUserProfile(); + + Future logout(); +} diff --git a/lib/features/auth/domain/use_cases/get_current_user_profile_use_case.dart b/lib/features/auth/domain/use_cases/get_current_user_profile_use_case.dart new file mode 100644 index 0000000..9f0490c --- /dev/null +++ b/lib/features/auth/domain/use_cases/get_current_user_profile_use_case.dart @@ -0,0 +1,10 @@ +import '../entities/user_profile.dart'; +import '../repositories/auth_repository.dart'; + +class GetCurrentUserProfileUseCase { + const GetCurrentUserProfileUseCase(this._repository); + + final AuthRepository _repository; + + Future call() => _repository.getCurrentUserProfile(); +} diff --git a/lib/features/auth/domain/use_cases/login_use_case.dart b/lib/features/auth/domain/use_cases/login_use_case.dart new file mode 100644 index 0000000..d824e72 --- /dev/null +++ b/lib/features/auth/domain/use_cases/login_use_case.dart @@ -0,0 +1,15 @@ +import '../entities/user_profile.dart'; +import '../repositories/auth_repository.dart'; + +class LoginUseCase { + const LoginUseCase(this._repository); + + final AuthRepository _repository; + + Future call({ + required String username, + required String password, + }) { + return _repository.login(username: username, password: password); + } +} diff --git a/lib/features/auth/domain/use_cases/logout_use_case.dart b/lib/features/auth/domain/use_cases/logout_use_case.dart new file mode 100644 index 0000000..166b6b8 --- /dev/null +++ b/lib/features/auth/domain/use_cases/logout_use_case.dart @@ -0,0 +1,9 @@ +import '../repositories/auth_repository.dart'; + +class LogoutUseCase { + const LogoutUseCase(this._repository); + + final AuthRepository _repository; + + Future call() => _repository.logout(); +} diff --git a/lib/features/auth/presentation/viewmodels/login_state.dart b/lib/features/auth/presentation/viewmodels/login_state.dart new file mode 100644 index 0000000..c116c8b --- /dev/null +++ b/lib/features/auth/presentation/viewmodels/login_state.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class LoginState { + const LoginState({ + this.username = '', + this.password = '', + this.obscurePassword = true, + this.isSubmitting = false, + this.errorMessage, + }); + + final String username; + final String password; + final bool obscurePassword; + final bool isSubmitting; + final String? errorMessage; + + static const _sentinel = Object(); + + LoginState copyWith({ + String? username, + String? password, + bool? obscurePassword, + bool? isSubmitting, + Object? errorMessage = _sentinel, + }) { + return LoginState( + username: username ?? this.username, + password: password ?? this.password, + obscurePassword: obscurePassword ?? this.obscurePassword, + isSubmitting: isSubmitting ?? this.isSubmitting, + errorMessage: identical(errorMessage, _sentinel) + ? this.errorMessage + : errorMessage as String?, + ); + } +} diff --git a/lib/features/auth/presentation/viewmodels/login_view_model.dart b/lib/features/auth/presentation/viewmodels/login_view_model.dart new file mode 100644 index 0000000..ccf7db2 --- /dev/null +++ b/lib/features/auth/presentation/viewmodels/login_view_model.dart @@ -0,0 +1,46 @@ +import 'package:cb_prestige_qr/features/auth/di/auth_providers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'login_state.dart'; + +final loginViewModelProvider = NotifierProvider( + LoginViewModel.new, +); + +class LoginViewModel extends Notifier { + @override + LoginState build() => const LoginState(); + + void updateUsername(String value) { + state = state.copyWith(username: value, errorMessage: null); + } + + void updatePassword(String value) { + state = state.copyWith(password: value, errorMessage: null); + } + + void togglePasswordVisibility() { + state = state.copyWith(obscurePassword: !state.obscurePassword); + } + + Future submit() async { + if (state.isSubmitting) return false; + + state = state.copyWith(isSubmitting: true, errorMessage: null); + + try { + await ref.read(loginUseCaseProvider)( + username: state.username.trim(), + password: state.password, + ); + state = state.copyWith(isSubmitting: false); + return true; + } catch (error) { + state = state.copyWith( + isSubmitting: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + return false; + } + } +} diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/views/login_view.dart similarity index 85% rename from lib/features/auth/presentation/pages/login_page.dart rename to lib/features/auth/presentation/views/login_view.dart index 6b39c3d..a130340 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/views/login_view.dart @@ -1,50 +1,24 @@ -import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; -import 'package:cb_prestige_qr/core/utils/MainShell.dart'; -import 'package:cb_prestige_qr/features/auth/data/user_profile_repository.dart'; +import 'package:cb_prestige_qr/app/navigation/main_shell.dart'; +import 'package:cb_prestige_qr/features/auth/presentation/viewmodels/login_view_model.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -class LoginPage extends StatefulWidget { - const LoginPage({super.key}); +class LoginView extends ConsumerStatefulWidget { + const LoginView({super.key}); @override - State createState() => _LoginPageState(); + ConsumerState createState() => _LoginViewState(); } -class _LoginPageState extends State { +class _LoginViewState extends ConsumerState { final _formKey = GlobalKey(); - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); - final UserProfileRepository _userProfileRepository = UserProfileRepository( - LocalStorageService(), - ); - bool _isSigningIn = false; - - var _obscurePassword = true; - - @override - void dispose() { - _usernameController.dispose(); - _passwordController.dispose(); - super.dispose(); - } Future _submit() async { final form = _formKey.currentState; if (form == null || !form.validate()) return; - setState(() { - _isSigningIn = true; - }); - - await Future.delayed(const Duration(seconds: 2)); - - if (!mounted) return; - - await _userProfileRepository.saveFromUsername( - _usernameController.text.trim(), - ); - - if (!mounted) return; + final didLogin = await ref.read(loginViewModelProvider.notifier).submit(); + if (!didLogin || !mounted) return; Navigator.of( context, @@ -55,6 +29,8 @@ class _LoginPageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final state = ref.watch(loginViewModelProvider); + final viewModel = ref.read(loginViewModelProvider.notifier); return Scaffold( body: Container( @@ -185,11 +161,12 @@ class _LoginPageState extends State { ), const SizedBox(height: 28), _AuthTextField( - controller: _usernameController, + initialValue: state.username, label: 'Username', hintText: 'Enter your username', keyboardType: TextInputType.text, prefixIcon: Icons.person_rounded, + onChanged: viewModel.updateUsername, validator: (value) { final username = value?.trim() ?? ''; if (username.isEmpty) { @@ -203,11 +180,12 @@ class _LoginPageState extends State { ), const SizedBox(height: 16), _AuthTextField( - controller: _passwordController, + initialValue: state.password, label: 'Password', hintText: 'Enter your password', prefixIcon: Icons.lock_rounded, - obscureText: _obscurePassword, + obscureText: state.obscurePassword, + onChanged: viewModel.updatePassword, validator: (value) { final password = value ?? ''; if (password.isEmpty) { @@ -219,13 +197,9 @@ class _LoginPageState extends State { return null; }, suffixIcon: IconButton( - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, + onPressed: viewModel.togglePasswordVisibility, icon: Icon( - _obscurePassword + state.obscurePassword ? Icons.visibility_off_rounded : Icons.visibility_rounded, color: Colors.white70, @@ -240,11 +214,21 @@ class _LoginPageState extends State { child: const Text('Forgot password?'), ), ), - const SizedBox(height: 24), + if (state.errorMessage != null) ...[ + Text( + state.errorMessage!, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.error, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 14), + ] else + const SizedBox(height: 6), SizedBox( width: double.infinity, child: FilledButton( - onPressed: _isSigningIn ? null : _submit, + onPressed: state.isSubmitting ? null : _submit, style: FilledButton.styleFrom( backgroundColor: colorScheme.primary, foregroundColor: Colors.white, @@ -253,7 +237,7 @@ class _LoginPageState extends State { borderRadius: BorderRadius.circular(18), ), ), - child: _isSigningIn + child: state.isSubmitting ? const SizedBox( height: 20, width: 20, @@ -288,21 +272,23 @@ class _LoginPageState extends State { class _AuthTextField extends StatelessWidget { const _AuthTextField({ - required this.controller, + required this.initialValue, required this.label, required this.hintText, required this.prefixIcon, required this.validator, + required this.onChanged, this.keyboardType, this.obscureText = false, this.suffixIcon, }); - final TextEditingController controller; + final String initialValue; final String label; final String hintText; final IconData prefixIcon; final String? Function(String?) validator; + final ValueChanged onChanged; final TextInputType? keyboardType; final bool obscureText; final Widget? suffixIcon; @@ -310,10 +296,11 @@ class _AuthTextField extends StatelessWidget { @override Widget build(BuildContext context) { return TextFormField( - controller: controller, + initialValue: initialValue, keyboardType: keyboardType, obscureText: obscureText, validator: validator, + onChanged: onChanged, style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: label, diff --git a/lib/features/history/presentation/manager/history_ui_state.dart b/lib/features/history/presentation/viewmodels/history_ui_state.dart similarity index 100% rename from lib/features/history/presentation/manager/history_ui_state.dart rename to lib/features/history/presentation/viewmodels/history_ui_state.dart diff --git a/lib/features/history/presentation/manager/history_view_model.dart b/lib/features/history/presentation/viewmodels/history_view_model.dart similarity index 100% rename from lib/features/history/presentation/manager/history_view_model.dart rename to lib/features/history/presentation/viewmodels/history_view_model.dart diff --git a/lib/features/history/presentation/pages/history_page.dart b/lib/features/history/presentation/views/history_view.dart similarity index 93% rename from lib/features/history/presentation/pages/history_page.dart rename to lib/features/history/presentation/views/history_view.dart index 975f10b..bc3a500 100644 --- a/lib/features/history/presentation/pages/history_page.dart +++ b/lib/features/history/presentation/views/history_view.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../manager/history_ui_state.dart'; -import '../manager/history_view_model.dart'; +import '../viewmodels/history_ui_state.dart'; +import '../viewmodels/history_view_model.dart'; import '../widgets/history_section_card.dart'; -class HistoryPage extends ConsumerWidget { - const HistoryPage({super.key}); +class HistoryView extends ConsumerWidget { + const HistoryView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/features/history/presentation/widgets/history_section_card.dart b/lib/features/history/presentation/widgets/history_section_card.dart index 02a29f2..417bf27 100644 --- a/lib/features/history/presentation/widgets/history_section_card.dart +++ b/lib/features/history/presentation/widgets/history_section_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../manager/history_ui_state.dart'; +import '../viewmodels/history_ui_state.dart'; import 'history_item_tile.dart'; class HistorySectionCard extends StatelessWidget { @@ -57,7 +57,7 @@ class HistorySectionCard extends StatelessWidget { ), const SizedBox(width: 10), Text( - '${section.totalTransactionsLabel} • ${section.totalAmountLabel}', + '${section.totalTransactionsLabel} | ${section.totalAmountLabel}', style: textTheme.labelMedium?.copyWith( color: colorScheme.onPrimary.withOpacity(0.9), fontWeight: FontWeight.w700, diff --git a/lib/features/home/data/data_sources/home_local_data_source.dart b/lib/features/home/data/data_sources/home_local_data_source.dart index 4b49c70..6979a03 100644 --- a/lib/features/home/data/data_sources/home_local_data_source.dart +++ b/lib/features/home/data/data_sources/home_local_data_source.dart @@ -28,4 +28,3 @@ class HomeLocalDataSourceImpl implements HomeLocalDataSource { ); } } - diff --git a/lib/features/home/data/models/recent_scan_model.dart b/lib/features/home/data/models/recent_scan_model.dart index 0e1f902..57e6823 100644 --- a/lib/features/home/data/models/recent_scan_model.dart +++ b/lib/features/home/data/models/recent_scan_model.dart @@ -1,9 +1,5 @@ import '../../domain/entities/recent_scan.dart'; class RecentScanModel extends RecentScan { - const RecentScanModel({ - required super.title, - required super.subtitle, - }); + const RecentScanModel({required super.title, required super.subtitle}); } - diff --git a/lib/features/home/data/repositories/home_repository_impl.dart b/lib/features/home/data/repositories/home_repository_impl.dart index 9d8d094..16a5dce 100644 --- a/lib/features/home/data/repositories/home_repository_impl.dart +++ b/lib/features/home/data/repositories/home_repository_impl.dart @@ -18,4 +18,3 @@ class HomeRepositoryImpl implements HomeRepository { ); } } - diff --git a/lib/features/home/domain/entities/home_content.dart b/lib/features/home/domain/entities/home_content.dart index 471cf20..3d371b8 100644 --- a/lib/features/home/domain/entities/home_content.dart +++ b/lib/features/home/domain/entities/home_content.dart @@ -1,12 +1,8 @@ import 'recent_scan.dart'; class HomeContent { - const HomeContent({ - required this.carouselAssets, - required this.recentScans, - }); + const HomeContent({required this.carouselAssets, required this.recentScans}); final List carouselAssets; final List recentScans; } - diff --git a/lib/features/home/domain/entities/recent_scan.dart b/lib/features/home/domain/entities/recent_scan.dart index a6721bb..41d3e8c 100644 --- a/lib/features/home/domain/entities/recent_scan.dart +++ b/lib/features/home/domain/entities/recent_scan.dart @@ -1,10 +1,6 @@ class RecentScan { - const RecentScan({ - required this.title, - required this.subtitle, - }); + const RecentScan({required this.title, required this.subtitle}); final String title; final String subtitle; } - diff --git a/lib/features/home/domain/repositories/home_repository.dart b/lib/features/home/domain/repositories/home_repository.dart index a9a08c4..e62b36b 100644 --- a/lib/features/home/domain/repositories/home_repository.dart +++ b/lib/features/home/domain/repositories/home_repository.dart @@ -3,4 +3,3 @@ import '../entities/home_content.dart'; abstract class HomeRepository { Future getHomeContent({int recentLimit = 5}); } - diff --git a/lib/features/home/domain/use_cases/get_home_content.dart b/lib/features/home/domain/use_cases/get_home_content.dart index 07cb99e..da3a7bb 100644 --- a/lib/features/home/domain/use_cases/get_home_content.dart +++ b/lib/features/home/domain/use_cases/get_home_content.dart @@ -10,4 +10,3 @@ class GetHomeContent { return _repository.getHomeContent(recentLimit: recentLimit); } } - diff --git a/lib/features/home/presentation/manager/home_ui_state.dart b/lib/features/home/presentation/viewmodels/home_ui_state.dart similarity index 100% rename from lib/features/home/presentation/manager/home_ui_state.dart rename to lib/features/home/presentation/viewmodels/home_ui_state.dart diff --git a/lib/features/home/presentation/manager/home_view_model.dart b/lib/features/home/presentation/viewmodels/home_view_model.dart similarity index 90% rename from lib/features/home/presentation/manager/home_view_model.dart rename to lib/features/home/presentation/viewmodels/home_view_model.dart index 9aa36aa..c0a7756 100644 --- a/lib/features/home/presentation/manager/home_view_model.dart +++ b/lib/features/home/presentation/viewmodels/home_view_model.dart @@ -42,11 +42,7 @@ class HomeViewModel extends AsyncNotifier { ); } - void onSeeAllTapped() { - // TODO: navigate to full list - } + void onSeeAllTapped() {} - void onRecentScanTapped(int index) { - // TODO: navigate to details - } + void onRecentScanTapped(int index) {} } diff --git a/lib/features/home/presentation/pages/home.dart b/lib/features/home/presentation/views/home_view.dart similarity index 90% rename from lib/features/home/presentation/pages/home.dart rename to lib/features/home/presentation/views/home_view.dart index d3ca565..22e3242 100644 --- a/lib/features/home/presentation/pages/home.dart +++ b/lib/features/home/presentation/views/home_view.dart @@ -1,11 +1,11 @@ import 'package:carousel_slider/carousel_slider.dart'; +import 'package:cb_prestige_qr/core/presentation/widgets/global_loading_overlay.dart'; import 'package:cb_prestige_qr/features/history/presentation/widgets/history_item_tile.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../../../core/presentation/widgets/global_loading_overlay.dart'; -import '../manager/home_ui_state.dart'; -import '../manager/home_view_model.dart'; +import '../viewmodels/home_ui_state.dart'; +import '../viewmodels/home_view_model.dart'; import '../widgets/home_today_summary_section.dart'; class HomeView extends ConsumerWidget { @@ -39,17 +39,17 @@ class HomeView extends ConsumerWidget { ], ), ), - body: stateAsync.when( - data: (state) => _HomeBody( - state: state, - onSeeAll: viewModel.onSeeAllTapped, - onRecentScanTap: viewModel.onRecentScanTapped, - ), - loading: () => const GlobalLoadingOverlay(isLoading: true), - error: (error, stackTrace) => Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( + body: stateAsync.when( + data: (state) => _HomeBody( + state: state, + onSeeAll: viewModel.onSeeAllTapped, + onRecentScanTap: viewModel.onRecentScanTapped, + ), + loading: () => const GlobalLoadingOverlay(isLoading: true), + error: (error, stackTrace) => Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Failed to load Home'), @@ -80,8 +80,7 @@ class _HomeBody extends StatelessWidget { @override Widget build(BuildContext context) { - final shellBottomInset = - 70 + MediaQuery.of(context).padding.bottom; + final shellBottomInset = 70 + MediaQuery.of(context).padding.bottom; final contentBottomPadding = shellBottomInset + 8; return CustomScrollView( diff --git a/lib/features/home/presentation/widgets/home_context_section.dart b/lib/features/home/presentation/widgets/home_context_section.dart index 5417418..947cd12 100644 --- a/lib/features/home/presentation/widgets/home_context_section.dart +++ b/lib/features/home/presentation/widgets/home_context_section.dart @@ -84,7 +84,6 @@ class HomeContextSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 2), Text( hasCustomer diff --git a/lib/features/home/presentation/widgets/home_today_summary_section.dart b/lib/features/home/presentation/widgets/home_today_summary_section.dart index e4a2327..68cd6cf 100644 --- a/lib/features/home/presentation/widgets/home_today_summary_section.dart +++ b/lib/features/home/presentation/widgets/home_today_summary_section.dart @@ -164,4 +164,3 @@ class _MiniStat extends StatelessWidget { ); } } - diff --git a/lib/features/injection_container.dart b/lib/features/injection_container.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source.dart b/lib/features/scan/data/data_sources/scan_remote_data_source.dart index 9fd1039..31e832e 100644 --- a/lib/features/scan/data/data_sources/scan_remote_data_source.dart +++ b/lib/features/scan/data/data_sources/scan_remote_data_source.dart @@ -1,8 +1,5 @@ import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart'; abstract interface class ScanRemoteDataSource { - Future> submitScan({ - required ScanSubmitPayload payload, - }); + Future> submitScan({required ScanSubmitPayload payload}); } - diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart b/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart index 77d9075..9024d20 100644 --- a/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart +++ b/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart @@ -12,4 +12,3 @@ class FakeScanRemoteDataSource implements ScanRemoteDataSource { }; } } - diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart b/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart index b7fd3b8..267a5b6 100644 --- a/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart +++ b/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart @@ -47,9 +47,7 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource { throw const FormatException('QR scan response must be a JSON object.'); } - final data = decoded.map( - (key, value) => MapEntry(key.toString(), value), - ); + final data = decoded.map((key, value) => MapEntry(key.toString(), value)); final isOk = data['OK'] == true || data['ok'] == true; if (!isOk) { throw HttpException( @@ -78,7 +76,9 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource { String _buildStatusMessage(int statusCode, String responseBody) { final normalizedBody = responseBody.trim(); - final bodySuffix = normalizedBody.isEmpty ? '' : ' Response: $normalizedBody'; + final bodySuffix = normalizedBody.isEmpty + ? '' + : ' Response: $normalizedBody'; return switch (statusCode) { HttpStatus.badRequest => @@ -91,8 +91,7 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource { 'Submit failed with status 404 (Not Found).$bodySuffix', HttpStatus.internalServerError => 'Submit failed with status 500 (Server Error).$bodySuffix', - _ => - 'Submit failed. Expected status 200 but got $statusCode.$bodySuffix', + _ => 'Submit failed. Expected status 200 but got $statusCode.$bodySuffix', }; } } diff --git a/lib/features/scan/data/models/scanned_qr_model.dart b/lib/features/scan/data/models/scanned_qr_model.dart index d3c0c2f..ae7d186 100644 --- a/lib/features/scan/data/models/scanned_qr_model.dart +++ b/lib/features/scan/data/models/scanned_qr_model.dart @@ -1,10 +1,7 @@ import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; class ScannedQrModel { - const ScannedQrModel({ - required this.rawValue, - required this.scannedAt, - }); + const ScannedQrModel({required this.rawValue, required this.scannedAt}); factory ScannedQrModel.fromJson(Map json) { return ScannedQrModel( @@ -27,4 +24,3 @@ class ScannedQrModel { return ScannedQr(rawValue: rawValue, scannedAt: scannedAt); } } - diff --git a/lib/features/scan/domain/entities/scan_submit_payload.dart b/lib/features/scan/domain/entities/scan_submit_payload.dart index d771de6..9880e30 100644 --- a/lib/features/scan/domain/entities/scan_submit_payload.dart +++ b/lib/features/scan/domain/entities/scan_submit_payload.dart @@ -7,9 +7,7 @@ class ScanSubmitPayload { required this.scannedByDeviceId, }); - factory ScanSubmitPayload.fromRawValue({ - required String rawValue, - }) { + factory ScanSubmitPayload.fromRawValue({required String rawValue}) { final payload = ScanSubmitPayload( token: rawValue.trim(), merchantId: 'merchant-001', diff --git a/lib/features/scan/domain/entities/scanned_qr.dart b/lib/features/scan/domain/entities/scanned_qr.dart index 330114f..6bd213b 100644 --- a/lib/features/scan/domain/entities/scanned_qr.dart +++ b/lib/features/scan/domain/entities/scanned_qr.dart @@ -1,10 +1,6 @@ class ScannedQr { - const ScannedQr({ - required this.rawValue, - required this.scannedAt, - }); + const ScannedQr({required this.rawValue, required this.scannedAt}); final String rawValue; final DateTime scannedAt; } - diff --git a/lib/features/scan/domain/use_cases/process_scan_use_case.dart b/lib/features/scan/domain/use_cases/process_scan_use_case.dart index 60683a7..54d9128 100644 --- a/lib/features/scan/domain/use_cases/process_scan_use_case.dart +++ b/lib/features/scan/domain/use_cases/process_scan_use_case.dart @@ -10,4 +10,3 @@ class ProcessScanUseCase { return _repository.processRawValue(rawValue); } } - diff --git a/lib/features/scan/presentation/manager/scan_controller.g.dart b/lib/features/scan/presentation/manager/scan_controller.g.dart deleted file mode 100644 index c8a504e..0000000 --- a/lib/features/scan/presentation/manager/scan_controller.g.dart +++ /dev/null @@ -1,62 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'scan_controller.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, type=warning - -@ProviderFor(ScanController) -final scanControllerProvider = ScanControllerProvider._(); - -final class ScanControllerProvider - extends $NotifierProvider { - ScanControllerProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'scanControllerProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$scanControllerHash(); - - @$internal - @override - ScanController create() => ScanController(); - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(ScanState value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$scanControllerHash() => r'5984074ff43ae1904e2025b355a689580d402a09'; - -abstract class _$ScanController extends $Notifier { - ScanState build(); - @$mustCallSuper - @override - void runBuild() { - final ref = this.ref as $Ref; - final element = - ref.element - as $ClassProviderElement< - AnyNotifier, - ScanState, - Object?, - Object? - >; - element.handleCreate(ref, build); - } -} diff --git a/lib/features/scan/scan_providers.dart b/lib/features/scan/presentation/providers/scan_providers.dart similarity index 51% rename from lib/features/scan/scan_providers.dart rename to lib/features/scan/presentation/providers/scan_providers.dart index c43b7e2..3b63b70 100644 --- a/lib/features/scan/scan_providers.dart +++ b/lib/features/scan/presentation/providers/scan_providers.dart @@ -3,22 +3,16 @@ import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_ import 'package:cb_prestige_qr/features/scan/data/repositories/scan_repository_impl.dart'; import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart'; import 'package:cb_prestige_qr/features/scan/domain/use_cases/process_scan_use_case.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -part 'scan_providers.g.dart'; +final scanRemoteDataSourceProvider = Provider( + (ref) => HttpScanRemoteDataSource(), +); -@riverpod -ScanRemoteDataSource scanRemoteDataSource(Ref ref) { - return HttpScanRemoteDataSource(); -} - -@riverpod -ScanRepository scanRepository(Ref ref) { - return ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider)); -} - -@riverpod -ProcessScanUseCase processScanUseCase(Ref ref) { - return ProcessScanUseCase(ref.watch(scanRepositoryProvider)); -} +final scanRepositoryProvider = Provider( + (ref) => ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider)), +); +final processScanUseCaseProvider = Provider( + (ref) => ProcessScanUseCase(ref.watch(scanRepositoryProvider)), +); diff --git a/lib/features/scan/presentation/manager/scan_controller.dart b/lib/features/scan/presentation/viewmodels/scan_controller.dart similarity index 81% rename from lib/features/scan/presentation/manager/scan_controller.dart rename to lib/features/scan/presentation/viewmodels/scan_controller.dart index 14c05ea..676d19e 100644 --- a/lib/features/scan/presentation/manager/scan_controller.dart +++ b/lib/features/scan/presentation/viewmodels/scan_controller.dart @@ -1,7 +1,9 @@ -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:cb_prestige_qr/features/scan/scan_providers.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/providers/scan_providers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -part 'scan_controller.g.dart'; +final scanControllerProvider = NotifierProvider( + ScanController.new, +); class ScanState { const ScanState({ @@ -41,8 +43,7 @@ class ScanState { } } -@riverpod -class ScanController extends _$ScanController { +class ScanController extends Notifier { @override ScanState build() => const ScanState(); @@ -60,12 +61,12 @@ class ScanController extends _$ScanController { isProcessing: false, scannedValue: result.rawValue, ); - } catch (e) { + } catch (error) { state = state.copyWith( isProcessing: false, isScanning: true, scannedValue: null, - errorMessage: e.toString(), + errorMessage: error.toString(), ); } } @@ -81,8 +82,11 @@ class ScanController extends _$ScanController { scannedValue: null, errorMessage: null, ); - } catch (e) { - state = state.copyWith(isSubmitting: false, errorMessage: e.toString()); + } catch (error) { + state = state.copyWith( + isSubmitting: false, + errorMessage: error.toString(), + ); rethrow; } } diff --git a/lib/core/utils/ScanShell.dart b/lib/features/scan/presentation/views/scan_flow.dart similarity index 77% rename from lib/core/utils/ScanShell.dart rename to lib/features/scan/presentation/views/scan_flow.dart index 05f9a97..3af4a8d 100644 --- a/lib/core/utils/ScanShell.dart +++ b/lib/features/scan/presentation/views/scan_flow.dart @@ -1,4 +1,4 @@ -import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_page.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/views/scan_page.dart'; import 'package:flutter/material.dart'; class ScanFlow extends StatelessWidget { diff --git a/lib/features/scan/presentation/pages/scan_page.dart b/lib/features/scan/presentation/views/scan_page.dart similarity index 97% rename from lib/features/scan/presentation/pages/scan_page.dart rename to lib/features/scan/presentation/views/scan_page.dart index 8308b61..194f929 100644 --- a/lib/features/scan/presentation/pages/scan_page.dart +++ b/lib/features/scan/presentation/views/scan_page.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:cb_prestige_qr/core/presentation/widgets/start_loading_overlay.dart'; -import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart'; -import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_result_page.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/viewmodels/scan_controller.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/views/scan_result_page.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; diff --git a/lib/features/scan/presentation/pages/scan_result_page.dart b/lib/features/scan/presentation/views/scan_result_page.dart similarity index 93% rename from lib/features/scan/presentation/pages/scan_result_page.dart rename to lib/features/scan/presentation/views/scan_result_page.dart index e5e35bc..02e9f52 100644 --- a/lib/features/scan/presentation/pages/scan_result_page.dart +++ b/lib/features/scan/presentation/views/scan_result_page.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:cb_prestige_qr/core/utils/MainShell.dart'; -import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart'; +import 'package:cb_prestige_qr/app/navigation/main_shell.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/viewmodels/scan_controller.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; @@ -150,9 +150,7 @@ class ScanResultPage extends ConsumerWidget { content: Text('QR submitted successfully.'), ), ); - ref - .read(navIndexNotifierProvider.notifier) - .setIndex(0); + ref.read(navIndexProvider.notifier).setIndex(0); ref .read(scanControllerProvider.notifier) .resumeScanning(); @@ -260,10 +258,17 @@ Map? _tryDecodeFromUri(String value) { return null; } -Map? _tryDecodeBase64Json(String value, {required bool urlSafe}) { +Map? _tryDecodeBase64Json( + String value, { + required bool urlSafe, +}) { try { - final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value); - final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized); + final normalized = urlSafe + ? base64Url.normalize(value) + : base64.normalize(value); + final bytes = urlSafe + ? base64Url.decode(normalized) + : base64.decode(normalized); final decoded = utf8.decode(bytes); final payload = jsonDecode(decoded); if (payload is! Map) return null; @@ -275,8 +280,12 @@ Map? _tryDecodeBase64Json(String value, {required bool urlSafe} String? _tryDecodeBase64Text(String value, {required bool urlSafe}) { try { - final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value); - final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized); + final normalized = urlSafe + ? base64Url.normalize(value) + : base64.normalize(value); + final bytes = urlSafe + ? base64Url.decode(normalized) + : base64.decode(normalized); final decoded = utf8.decode(bytes).trim(); if (decoded.isEmpty || decoded == value) return null; return decoded; diff --git a/lib/features/scan/scan_providers.g.dart b/lib/features/scan/scan_providers.g.dart deleted file mode 100644 index cca690e..0000000 --- a/lib/features/scan/scan_providers.g.dart +++ /dev/null @@ -1,147 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'scan_providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, type=warning - -@ProviderFor(scanRemoteDataSource) -final scanRemoteDataSourceProvider = ScanRemoteDataSourceProvider._(); - -final class ScanRemoteDataSourceProvider - extends - $FunctionalProvider< - ScanRemoteDataSource, - ScanRemoteDataSource, - ScanRemoteDataSource - > - with $Provider { - ScanRemoteDataSourceProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'scanRemoteDataSourceProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$scanRemoteDataSourceHash(); - - @$internal - @override - $ProviderElement $createElement( - $ProviderPointer pointer, - ) => $ProviderElement(pointer); - - @override - ScanRemoteDataSource create(Ref ref) { - return scanRemoteDataSource(ref); - } - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(ScanRemoteDataSource value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$scanRemoteDataSourceHash() => - r'f25d69a350b64c4dd0cf8ef124838bb351b5e4a1'; - -@ProviderFor(scanRepository) -final scanRepositoryProvider = ScanRepositoryProvider._(); - -final class ScanRepositoryProvider - extends $FunctionalProvider - with $Provider { - ScanRepositoryProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'scanRepositoryProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$scanRepositoryHash(); - - @$internal - @override - $ProviderElement $createElement($ProviderPointer pointer) => - $ProviderElement(pointer); - - @override - ScanRepository create(Ref ref) { - return scanRepository(ref); - } - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(ScanRepository value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$scanRepositoryHash() => r'3e719b962dbb1014143710238128234371ef141e'; - -@ProviderFor(processScanUseCase) -final processScanUseCaseProvider = ProcessScanUseCaseProvider._(); - -final class ProcessScanUseCaseProvider - extends - $FunctionalProvider< - ProcessScanUseCase, - ProcessScanUseCase, - ProcessScanUseCase - > - with $Provider { - ProcessScanUseCaseProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'processScanUseCaseProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$processScanUseCaseHash(); - - @$internal - @override - $ProviderElement $createElement( - $ProviderPointer pointer, - ) => $ProviderElement(pointer); - - @override - ProcessScanUseCase create(Ref ref) { - return processScanUseCase(ref); - } - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(ProcessScanUseCase value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$processScanUseCaseHash() => - r'ce9f6a3d0de27fe0f7f8e7fa1ac9c1f88ad57e43'; diff --git a/lib/features/settings/data/data_sources/settings_local_data_source.dart b/lib/features/settings/data/data_sources/settings_local_data_source.dart index 461aa6e..42500a0 100644 --- a/lib/features/settings/data/data_sources/settings_local_data_source.dart +++ b/lib/features/settings/data/data_sources/settings_local_data_source.dart @@ -1,5 +1,6 @@ import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; -import 'package:cb_prestige_qr/features/auth/data/user_profile_repository.dart'; +import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_local_data_source.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart'; import '../../domain/entities/settings_content.dart'; @@ -10,32 +11,46 @@ abstract class SettingsLocalDataSource { } class SettingsLocalDataSourceImpl implements SettingsLocalDataSource { - SettingsLocalDataSourceImpl(this._storage); + SettingsLocalDataSourceImpl(this._storage, this._authLocalDataSource); final LocalStorageService _storage; + final AuthLocalDataSource _authLocalDataSource; - bool _notificationsEnabled = true; - bool _hapticsEnabled = true; + static const _notificationsKey = 'settings_notifications_enabled'; + static const _hapticsKey = 'settings_haptics_enabled'; @override Future getSettings() async { - final userProfile = await UserProfileRepository(_storage).load(); + final userProfile = await _authLocalDataSource.getSavedUserProfile(); return SettingsContent( - notificationsEnabled: _notificationsEnabled, - hapticsEnabled: _hapticsEnabled, + notificationsEnabled: await _storage.getBool(_notificationsKey) ?? true, + hapticsEnabled: await _storage.getBool(_hapticsKey) ?? true, appVersionLabel: 'v1.0.0', - userProfile: userProfile, + userProfile: userProfile ?? const _FallbackUserProfileFactory().create(), ); } @override Future setNotificationsEnabled(bool value) async { - _notificationsEnabled = value; + await _storage.setBool(_notificationsKey, value); } @override Future setHapticsEnabled(bool value) async { - _hapticsEnabled = value; + await _storage.setBool(_hapticsKey, value); + } +} + +class _FallbackUserProfileFactory { + const _FallbackUserProfileFactory(); + + UserProfile create() { + return const UserProfile( + username: 'cashier', + displayName: 'Cashier', + roleLabel: 'Cashier', + branchLabel: 'Prestige Counter', + ); } } diff --git a/lib/features/settings/domain/entities/settings_content.dart b/lib/features/settings/domain/entities/settings_content.dart index b54e09a..7633de5 100644 --- a/lib/features/settings/domain/entities/settings_content.dart +++ b/lib/features/settings/domain/entities/settings_content.dart @@ -1,4 +1,4 @@ -import 'package:cb_prestige_qr/features/auth/domain/user_profile.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart'; class SettingsContent { const SettingsContent({ diff --git a/lib/features/settings/presentation/manager/settings_ui_state.dart b/lib/features/settings/presentation/viewmodels/settings_ui_state.dart similarity index 91% rename from lib/features/settings/presentation/manager/settings_ui_state.dart rename to lib/features/settings/presentation/viewmodels/settings_ui_state.dart index e933b39..be5c4c6 100644 --- a/lib/features/settings/presentation/manager/settings_ui_state.dart +++ b/lib/features/settings/presentation/viewmodels/settings_ui_state.dart @@ -1,7 +1,6 @@ +import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart'; import 'package:flutter/foundation.dart'; -import '../../../auth/domain/user_profile.dart'; - @immutable class SettingsUiState { const SettingsUiState({ diff --git a/lib/features/settings/presentation/manager/settings_view_model.dart b/lib/features/settings/presentation/viewmodels/settings_view_model.dart similarity index 81% rename from lib/features/settings/presentation/manager/settings_view_model.dart rename to lib/features/settings/presentation/viewmodels/settings_view_model.dart index 873306c..d0d4e7b 100644 --- a/lib/features/settings/presentation/manager/settings_view_model.dart +++ b/lib/features/settings/presentation/viewmodels/settings_view_model.dart @@ -1,6 +1,7 @@ +import 'package:cb_prestige_qr/core/providers/core_providers.dart'; +import 'package:cb_prestige_qr/features/auth/di/auth_providers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../../../core/storage/local_storage_service.dart'; import '../../data/data_sources/settings_local_data_source.dart'; import '../../data/repositories/settings_repository_impl.dart'; import '../../domain/entities/settings_content.dart'; @@ -10,12 +11,11 @@ import '../../domain/use_cases/set_haptics_enabled.dart'; import '../../domain/use_cases/set_notifications_enabled.dart'; import 'settings_ui_state.dart'; -final _localStorageServiceProvider = Provider( - (ref) => LocalStorageService(), -); - final _settingsLocalDataSourceProvider = Provider( - (ref) => SettingsLocalDataSourceImpl(ref.watch(_localStorageServiceProvider)), + (ref) => SettingsLocalDataSourceImpl( + ref.watch(localStorageServiceProvider), + ref.watch(authLocalDataSourceProvider), + ), ); final _settingsRepositoryProvider = Provider( @@ -56,12 +56,16 @@ class SettingsViewModel extends AsyncNotifier { } Future toggleNotifications(bool value) async { - await ref.watch(_setNotificationsEnabledProvider)(value); + await ref.read(_setNotificationsEnabledProvider)(value); state = state.whenData((s) => s.copyWith(notificationsEnabled: value)); } Future toggleHaptics(bool value) async { - await ref.watch(_setHapticsEnabledProvider)(value); + await ref.read(_setHapticsEnabledProvider)(value); state = state.whenData((s) => s.copyWith(hapticsEnabled: value)); } + + Future logout() async { + await ref.read(logoutUseCaseProvider)(); + } } diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/views/settings_view.dart similarity index 92% rename from lib/features/settings/presentation/pages/settings_page.dart rename to lib/features/settings/presentation/views/settings_view.dart index 3453bf4..361b3b7 100644 --- a/lib/features/settings/presentation/pages/settings_page.dart +++ b/lib/features/settings/presentation/views/settings_view.dart @@ -1,14 +1,14 @@ -import 'package:cb_prestige_qr/core/utils/MainShell.dart'; -import 'package:cb_prestige_qr/features/auth/presentation/pages/login_page.dart'; +import 'package:cb_prestige_qr/app/navigation/main_shell.dart'; +import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart'; +import 'package:cb_prestige_qr/features/auth/presentation/views/login_view.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../../auth/domain/user_profile.dart'; -import '../manager/settings_ui_state.dart'; -import '../manager/settings_view_model.dart'; +import '../viewmodels/settings_ui_state.dart'; +import '../viewmodels/settings_view_model.dart'; -class SettingsPage extends ConsumerWidget { - const SettingsPage({super.key}); +class SettingsView extends ConsumerWidget { + const SettingsView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -31,11 +31,14 @@ class SettingsPage extends ConsumerWidget { onNotificationsChanged: viewModel.toggleNotifications, onHapticsChanged: viewModel.toggleHaptics, onLogout: () { - ref.read(navIndexNotifierProvider.notifier).setIndex(0); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const LoginPage()), - (route) => false, - ); + viewModel.logout().then((_) { + if (!context.mounted) return; + ref.read(navIndexProvider.notifier).setIndex(0); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginView()), + (route) => false, + ); + }); }, ), loading: () => const Center(child: CircularProgressIndicator()), diff --git a/lib/features/splash/presentation/pages/splash_screen.dart b/lib/features/splash/presentation/pages/splash_screen.dart index ec963b8..9a394fc 100644 --- a/lib/features/splash/presentation/pages/splash_screen.dart +++ b/lib/features/splash/presentation/pages/splash_screen.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:cb_prestige_qr/core/utils/MainShell.dart'; +import 'package:cb_prestige_qr/app/navigation/main_shell.dart'; import 'package:flutter/material.dart'; class SplashScreen extends StatefulWidget { @@ -26,9 +26,9 @@ class _SplashScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _timer = Timer(widget.duration, () { if (!mounted) return; - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const MainShell()), - ); + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (_) => const MainShell())); }); }); } @@ -43,12 +43,8 @@ class _SplashScreenState extends State { Widget build(BuildContext context) { return Scaffold( body: SizedBox.expand( - child: Image.asset( - widget.imageAssetPath, - fit: BoxFit.cover, - ), + child: Image.asset(widget.imageAssetPath, fit: BoxFit.cover), ), ); } } - diff --git a/lib/main.dart b/lib/main.dart index 48aae98..96699d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:cb_prestige_qr/features/auth/presentation/pages/login_page.dart'; +import 'package:cb_prestige_qr/app/app.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -6,56 +6,3 @@ void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const ProviderScope(child: MyApp())); } - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'CB Prestige Banking', - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xff896e4b), - brightness: Brightness.dark, - ), - scaffoldBackgroundColor: const Color(0xff25262b), - appBarTheme: const AppBarTheme( - backgroundColor: Color(0xff896e4b), - foregroundColor: Colors.white, - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: const Color(0xff2a2b31), - labelStyle: const TextStyle(color: Colors.white70), - hintStyle: const TextStyle(color: Colors.white38), - prefixIconColor: Colors.white70, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: BorderSide(color: Colors.white.withOpacity(0.06)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: const BorderSide(color: Color(0xff896e4b), width: 1.2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: const BorderSide(color: Colors.redAccent), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: const BorderSide(color: Colors.redAccent), - ), - ), - ), - home: const LoginPage(), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 7109eab..55e7404 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" fake_async: dependency: transitive description: @@ -287,7 +303,7 @@ packages: source: hosted version: "3.3.1" flutter_svg: - dependency: "direct dev" + dependency: "direct main" description: name: flutter_svg sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" diff --git a/pubspec.yaml b/pubspec.yaml index 5806c05..1fa8f44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: mobile_scanner: ^7.2.0 shared_preferences: ^2.5.5 jwt_decoder: ^2.0.1 + dio: ^5.9.0 + flutter_svg: ^2.2.4 dev_dependencies: flutter_test: @@ -55,7 +57,6 @@ dev_dependencies: flutter_lints: ^6.0.0 riverpod_generator: ^4.0.3 build_runner: ^2.13.1 - flutter_svg: ^2.2.4 flutter_native_splash: 2.4.7 diff --git a/test/widget_test.dart b/test/widget_test.dart index 69efdcf..152d663 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,21 +1,20 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. -import 'package:cb_prestige_qr/main.dart'; +import 'package:cb_prestige_qr/app/app.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; void main() { testWidgets('shows login form fields', (WidgetTester tester) async { - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const ProviderScope(child: MyApp())); - expect(find.text('Login'), findsOneWidget); - expect(find.text('Phone Number'), findsOneWidget); - expect(find.text('User ID (Username)'), findsOneWidget); + expect(find.text('Secure Login'), findsOneWidget); + expect(find.text('Username'), findsOneWidget); expect(find.text('Password'), findsOneWidget); expect(find.text('Sign In'), findsOneWidget); });