folder repair

This commit is contained in:
moon 2026-04-27 15:38:26 +06:30
parent 7da04ab49d
commit cf948158d4
72 changed files with 805 additions and 686 deletions

17
lib/app/app.dart Normal file
View File

@ -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(),
);
}
}

View File

@ -1,18 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; 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/presentation/widgets/global_loading_overlay.dart';
import 'package:cb_prestige_qr/core/widgets/CenterNavButton.dart'; import 'package:cb_prestige_qr/core/widgets/center_nav_button.dart';
import 'package:cb_prestige_qr/core/widgets/NavItem.dart'; import 'package:cb_prestige_qr/core/widgets/nav_item.dart';
import 'package:cb_prestige_qr/features/analysis/presentation/pages/analysis_page.dart'; import 'package:cb_prestige_qr/features/analysis/presentation/views/analysis_view.dart';
import 'package:cb_prestige_qr/features/history/presentation/pages/history_page.dart'; import 'package:cb_prestige_qr/features/history/presentation/views/history_view.dart';
import 'package:cb_prestige_qr/features/home/presentation/pages/home.dart'; import 'package:cb_prestige_qr/features/home/presentation/views/home_view.dart';
import 'package:cb_prestige_qr/features/settings/presentation/pages/settings_page.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:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
final navIndexNotifierProvider = NotifierProvider<NavIndexNotifier, int>( final navIndexProvider = NotifierProvider<NavIndexNotifier, int>(
NavIndexNotifier.new, NavIndexNotifier.new,
); );
@ -39,7 +39,9 @@ class _MainShellState extends ConsumerState<MainShell> {
void _startLoading([Duration duration = const Duration(seconds: 1)]) { void _startLoading([Duration duration = const Duration(seconds: 1)]) {
_timer?.cancel(); _timer?.cancel();
if (!_isLoading) setState(() => _isLoading = true); if (!_isLoading) {
setState(() => _isLoading = true);
}
_timer = Timer(duration, () { _timer = Timer(duration, () {
if (!mounted) return; if (!mounted) return;
setState(() => _isLoading = false); setState(() => _isLoading = false);
@ -49,8 +51,10 @@ class _MainShellState extends ConsumerState<MainShell> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final initialIndex = ref.read(navIndexNotifierProvider); final initialIndex = ref.read(navIndexProvider);
if (initialIndex != 0) _startLoading(); if (initialIndex != 0) {
_startLoading();
}
} }
@override @override
@ -61,21 +65,20 @@ class _MainShellState extends ConsumerState<MainShell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedIndex = ref.watch(navIndexNotifierProvider); final selectedIndex = ref.watch(navIndexProvider);
final theme = Theme.of(context); final theme = Theme.of(context);
ref.listen<int>(navIndexNotifierProvider, (previous, next) { ref.listen<int>(navIndexProvider, (previous, next) {
if (previous == null || previous == next) return; if (previous == null || previous == next || next == 0) return;
if (next == 0) return;
_startLoading(); _startLoading();
}); });
final pages = [ const pages = <Widget>[
const HomeView(), HomeView(),
const AnalysisPage(), AnalysisView(),
const SizedBox(), SizedBox(),
const HistoryPage(), HistoryView(),
const SettingsPage(), SettingsView(),
]; ];
return Stack( return Stack(
@ -113,18 +116,16 @@ class _MainShellState extends ConsumerState<MainShell> {
child: NavItem( child: NavItem(
icon: Icons.home_rounded, icon: Icons.home_rounded,
isActive: selectedIndex == 0, isActive: selectedIndex == 0,
onTap: () => ref onTap: () =>
.read(navIndexNotifierProvider.notifier) ref.read(navIndexProvider.notifier).setIndex(0),
.setIndex(0),
), ),
), ),
Expanded( Expanded(
child: NavItem( child: NavItem(
icon: Icons.analytics, icon: Icons.analytics,
isActive: selectedIndex == 1, isActive: selectedIndex == 1,
onTap: () => ref onTap: () =>
.read(navIndexNotifierProvider.notifier) ref.read(navIndexProvider.notifier).setIndex(1),
.setIndex(1),
), ),
), ),
Expanded( Expanded(
@ -144,18 +145,16 @@ class _MainShellState extends ConsumerState<MainShell> {
child: NavItem( child: NavItem(
icon: Icons.history_rounded, icon: Icons.history_rounded,
isActive: selectedIndex == 3, isActive: selectedIndex == 3,
onTap: () => ref onTap: () =>
.read(navIndexNotifierProvider.notifier) ref.read(navIndexProvider.notifier).setIndex(3),
.setIndex(3),
), ),
), ),
Expanded( Expanded(
child: NavItem( child: NavItem(
icon: Icons.person_rounded, icon: Icons.person_rounded,
isActive: selectedIndex == 4, isActive: selectedIndex == 4,
onTap: () => ref onTap: () =>
.read(navIndexNotifierProvider.notifier) ref.read(navIndexProvider.notifier).setIndex(4),
.setIndex(4),
), ),
), ),
], ],

View File

@ -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),
),
),
);
}
}

View File

@ -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',
},
),
);
}
}

View File

@ -2,10 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class GlobalLoadingOverlay extends StatelessWidget { class GlobalLoadingOverlay extends StatelessWidget {
const GlobalLoadingOverlay({ const GlobalLoadingOverlay({super.key, required this.isLoading});
super.key,
required this.isLoading,
});
final bool isLoading; final bool isLoading;
@ -22,12 +19,8 @@ class GlobalLoadingOverlay extends StatelessWidget {
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: const SizedBox.expand(), child: const SizedBox.expand(),
), ),
ColoredBox( ColoredBox(color: Colors.black.withOpacity(0.55)),
color: Colors.black.withOpacity(0.55), const Center(child: _AnimatedLoader()),
),
const Center(
child: _AnimatedLoader(),
),
], ],
), ),
), ),
@ -59,30 +52,15 @@ class _AnimatedLoaderState extends State<_AnimatedLoader>
/// Main fade (smooth breathing) /// Main fade (smooth breathing)
_mainOpacity = TweenSequence<double>([ _mainOpacity = TweenSequence<double>([
TweenSequenceItem( TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 50),
tween: Tween(begin: 0.0, end: 1.0), TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.0), weight: 50),
weight: 50, ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
),
TweenSequenceItem(
tween: Tween(begin: 1.0, end: 0.0),
weight: 50,
),
]).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
/// Glow fade (slightly offset feel) /// Glow fade (slightly offset feel)
_glowOpacity = Tween<double>( _glowOpacity = Tween<double>(
begin: 0.2, begin: 0.2,
end: 0.5, end: 0.5,
).animate( ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
} }
@override @override

View File

@ -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<LocalStorageService>(
(ref) => LocalStorageService(),
);
final deviceIdServiceProvider = Provider<DeviceIdService>(
(ref) => DeviceIdService(),
);
final dioProvider = Provider<Dio>((ref) => ApiClient.create());

View File

@ -1,16 +1,14 @@
import 'package:flutter/services.dart';
class DeviceIdService { class DeviceIdService {
static const _channel = MethodChannel('cb_prestige_qr/device');
Future<String> getDeviceId() async { Future<String> getDeviceId() async {
// final deviceId = await _channel.invokeMethod<String>('getDeviceId'); // final deviceId = await const MethodChannel(
// 'cb_prestige_qr/device',
// ).invokeMethod<String>('getDeviceId');
// if (deviceId == null || deviceId.trim().isEmpty) { // if (deviceId == null || deviceId.trim().isEmpty) {
// throw PlatformException( // throw PlatformException(
// code: 'device_id_unavailable', // code: 'device_id_unavailable',
// message: 'Unable to retrieve device ID.', // message: 'Unable to retrieve device ID.',
// ); // );
// } // }
return "demo_device_id"; return 'demo_device_id';
} }
} }

View File

@ -20,4 +20,9 @@ class LocalStorageService {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
return prefs.getString(key); return prefs.getString(key);
} }
Future<void> remove(String key) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
}
} }

View File

@ -1,15 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CenterNavButton extends StatelessWidget { class CenterNavButton extends StatelessWidget {
final bool isActive;
final VoidCallback onTap;
const CenterNavButton({ const CenterNavButton({
super.key, super.key,
required this.isActive, required this.isActive,
required this.onTap, required this.onTap,
}); });
final bool isActive;
final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);

View File

@ -1,10 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class NavItem extends StatelessWidget { class NavItem extends StatelessWidget {
final IconData icon;
final bool isActive;
final VoidCallback onTap;
const NavItem({ const NavItem({
super.key, super.key,
required this.icon, required this.icon,
@ -12,6 +8,10 @@ class NavItem extends StatelessWidget {
required this.onTap, required this.onTap,
}); });
final IconData icon;
final bool isActive;
final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);

View File

@ -40,7 +40,7 @@ class AnalysisSummaryUiModel {
@immutable @immutable
class AnalysisUiState { class AnalysisUiState {
AnalysisUiState({ const AnalysisUiState({
int? rangeDays, int? rangeDays,
String? rangeLabel, String? rangeLabel,
AnalysisSummaryUiModel? summary, AnalysisSummaryUiModel? summary,
@ -62,7 +62,6 @@ class AnalysisUiState {
final String? rangeLabel; final String? rangeLabel;
final AnalysisSummaryUiModel? summary; final AnalysisSummaryUiModel? summary;
final List<AnalysisSeriesPointUiModel>? series; final List<AnalysisSeriesPointUiModel>? series;
final double? averageScansPerDay; final double? averageScansPerDay;
final double? averagePointsPerDay; final double? averagePointsPerDay;
final double? pointsPerScan; final double? pointsPerScan;

View File

@ -9,12 +9,12 @@ import 'analysis_chart_metric.dart';
import 'analysis_range.dart'; import 'analysis_range.dart';
import 'analysis_ui_state.dart'; import 'analysis_ui_state.dart';
final analysisRangeNotifierProvider = final analysisRangeProvider =
NotifierProvider<AnalysisRangeNotifier, AnalysisRangePreset>( NotifierProvider<AnalysisRangeNotifier, AnalysisRangePreset>(
AnalysisRangeNotifier.new, AnalysisRangeNotifier.new,
); );
final analysisChartMetricNotifierProvider = final analysisChartMetricProvider =
NotifierProvider<AnalysisChartMetricNotifier, AnalysisChartMetric>( NotifierProvider<AnalysisChartMetricNotifier, AnalysisChartMetric>(
AnalysisChartMetricNotifier.new, AnalysisChartMetricNotifier.new,
); );
@ -53,7 +53,7 @@ final analysisViewModelProvider =
class AnalysisViewModel extends AsyncNotifier<AnalysisUiState> { class AnalysisViewModel extends AsyncNotifier<AnalysisUiState> {
@override @override
Future<AnalysisUiState> build() async { Future<AnalysisUiState> build() async {
final rangePreset = ref.watch(analysisRangeNotifierProvider); final rangePreset = ref.watch(analysisRangeProvider);
final content = await ref.watch(_getAnalysisContentProvider)( final content = await ref.watch(_getAnalysisContentProvider)(
rangeDays: rangePreset.days, rangeDays: rangePreset.days,
); );

View File

@ -1,20 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../manager/analysis_chart_metric.dart'; import '../viewmodels/analysis_chart_metric.dart';
import '../manager/analysis_range.dart'; import '../viewmodels/analysis_range.dart';
import '../manager/analysis_ui_state.dart'; import '../viewmodels/analysis_ui_state.dart';
import '../manager/analysis_view_model.dart'; import '../viewmodels/analysis_view_model.dart';
import '../widgets/analysis_line_chart.dart'; import '../widgets/analysis_line_chart.dart';
class AnalysisPage extends ConsumerWidget { class AnalysisView extends ConsumerWidget {
const AnalysisPage({super.key}); const AnalysisView({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final rangePreset = ref.watch(analysisRangeNotifierProvider); final rangePreset = ref.watch(analysisRangeProvider);
final chartMetric = ref.watch(analysisChartMetricNotifierProvider); final chartMetric = ref.watch(analysisChartMetricProvider);
final stateAsync = ref.watch(analysisViewModelProvider); final stateAsync = ref.watch(analysisViewModelProvider);
return Scaffold( return Scaffold(
@ -34,9 +34,8 @@ class AnalysisPage extends ConsumerWidget {
children: [ children: [
_RangeSelector( _RangeSelector(
selected: rangePreset, selected: rangePreset,
onSelected: (preset) => ref onSelected: (preset) =>
.read(analysisRangeNotifierProvider.notifier) ref.read(analysisRangeProvider.notifier).setRange(preset),
.setRange(preset),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
stateAsync.when( stateAsync.when(
@ -44,7 +43,7 @@ class AnalysisPage extends ConsumerWidget {
state: state, state: state,
chartMetric: chartMetric, chartMetric: chartMetric,
onMetricSelected: (metric) => ref onMetricSelected: (metric) => ref
.read(analysisChartMetricNotifierProvider.notifier) .read(analysisChartMetricProvider.notifier)
.setMetric(metric), .setMetric(metric),
), ),
loading: () => const _LoadingBody(), loading: () => const _LoadingBody(),

View File

@ -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<void> saveSession(User user);
Future<UserProfile?> getSavedUserProfile();
Future<void> 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<void> 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<UserProfile?> 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<void> 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)}';
}
}

View File

@ -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<LoginResponseModel> login(LoginRequestModel request);
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
const AuthRemoteDataSourceImpl();
@override
Future<LoginResponseModel> login(LoginRequestModel request) async {
await Future<void>.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)}';
}
}

View File

@ -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<String, dynamic> toJson() {
return <String, dynamic>{
'username': username,
'password': password,
'device_id': deviceId,
};
}
}

View File

@ -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;
}

View File

@ -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<UserProfile> 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<UserProfile?> getCurrentUserProfile() {
return _localDataSource.getSavedUserProfile();
}
@override
Future<void> logout() => _localDataSource.clearSession();
}

View File

@ -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<UserProfile> 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<void> 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)}';
}
}

View File

@ -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<AuthLocalDataSource>(
(ref) => AuthLocalDataSourceImpl(ref.watch(localStorageServiceProvider)),
);
final authRemoteDataSourceProvider = Provider<AuthRemoteDataSource>(
(ref) => const AuthRemoteDataSourceImpl(),
);
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepositoryImpl(
ref.watch(authRemoteDataSourceProvider),
ref.watch(authLocalDataSourceProvider),
ref.watch(deviceIdServiceProvider),
),
);
final loginUseCaseProvider = Provider<LoginUseCase>(
(ref) => LoginUseCase(ref.watch(authRepositoryProvider)),
);
final getCurrentUserProfileUseCaseProvider =
Provider<GetCurrentUserProfileUseCase>(
(ref) => GetCurrentUserProfileUseCase(ref.watch(authRepositoryProvider)),
);
final logoutUseCaseProvider = Provider<LogoutUseCase>(
(ref) => LogoutUseCase(ref.watch(authRepositoryProvider)),
);

View File

@ -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;
}

View File

@ -0,0 +1,12 @@
import '../entities/user_profile.dart';
abstract class AuthRepository {
Future<UserProfile> login({
required String username,
required String password,
});
Future<UserProfile?> getCurrentUserProfile();
Future<void> logout();
}

View File

@ -0,0 +1,10 @@
import '../entities/user_profile.dart';
import '../repositories/auth_repository.dart';
class GetCurrentUserProfileUseCase {
const GetCurrentUserProfileUseCase(this._repository);
final AuthRepository _repository;
Future<UserProfile?> call() => _repository.getCurrentUserProfile();
}

View File

@ -0,0 +1,15 @@
import '../entities/user_profile.dart';
import '../repositories/auth_repository.dart';
class LoginUseCase {
const LoginUseCase(this._repository);
final AuthRepository _repository;
Future<UserProfile> call({
required String username,
required String password,
}) {
return _repository.login(username: username, password: password);
}
}

View File

@ -0,0 +1,9 @@
import '../repositories/auth_repository.dart';
class LogoutUseCase {
const LogoutUseCase(this._repository);
final AuthRepository _repository;
Future<void> call() => _repository.logout();
}

View File

@ -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?,
);
}
}

View File

@ -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, LoginState>(
LoginViewModel.new,
);
class LoginViewModel extends Notifier<LoginState> {
@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<bool> 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;
}
}
}

View File

@ -1,50 +1,24 @@
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
import 'package:cb_prestige_qr/core/utils/MainShell.dart'; import 'package:cb_prestige_qr/features/auth/presentation/viewmodels/login_view_model.dart';
import 'package:cb_prestige_qr/features/auth/data/user_profile_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class LoginPage extends StatefulWidget { class LoginView extends ConsumerStatefulWidget {
const LoginPage({super.key}); const LoginView({super.key});
@override @override
State<LoginPage> createState() => _LoginPageState(); ConsumerState<LoginView> createState() => _LoginViewState();
} }
class _LoginPageState extends State<LoginPage> { class _LoginViewState extends ConsumerState<LoginView> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
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<void> _submit() async { Future<void> _submit() async {
final form = _formKey.currentState; final form = _formKey.currentState;
if (form == null || !form.validate()) return; if (form == null || !form.validate()) return;
setState(() { final didLogin = await ref.read(loginViewModelProvider.notifier).submit();
_isSigningIn = true; if (!didLogin || !mounted) return;
});
await Future<void>.delayed(const Duration(seconds: 2));
if (!mounted) return;
await _userProfileRepository.saveFromUsername(
_usernameController.text.trim(),
);
if (!mounted) return;
Navigator.of( Navigator.of(
context, context,
@ -55,6 +29,8 @@ class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final state = ref.watch(loginViewModelProvider);
final viewModel = ref.read(loginViewModelProvider.notifier);
return Scaffold( return Scaffold(
body: Container( body: Container(
@ -185,11 +161,12 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(height: 28), const SizedBox(height: 28),
_AuthTextField( _AuthTextField(
controller: _usernameController, initialValue: state.username,
label: 'Username', label: 'Username',
hintText: 'Enter your username', hintText: 'Enter your username',
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
prefixIcon: Icons.person_rounded, prefixIcon: Icons.person_rounded,
onChanged: viewModel.updateUsername,
validator: (value) { validator: (value) {
final username = value?.trim() ?? ''; final username = value?.trim() ?? '';
if (username.isEmpty) { if (username.isEmpty) {
@ -203,11 +180,12 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_AuthTextField( _AuthTextField(
controller: _passwordController, initialValue: state.password,
label: 'Password', label: 'Password',
hintText: 'Enter your password', hintText: 'Enter your password',
prefixIcon: Icons.lock_rounded, prefixIcon: Icons.lock_rounded,
obscureText: _obscurePassword, obscureText: state.obscurePassword,
onChanged: viewModel.updatePassword,
validator: (value) { validator: (value) {
final password = value ?? ''; final password = value ?? '';
if (password.isEmpty) { if (password.isEmpty) {
@ -219,13 +197,9 @@ class _LoginPageState extends State<LoginPage> {
return null; return null;
}, },
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () { onPressed: viewModel.togglePasswordVisibility,
setState(() {
_obscurePassword = !_obscurePassword;
});
},
icon: Icon( icon: Icon(
_obscurePassword state.obscurePassword
? Icons.visibility_off_rounded ? Icons.visibility_off_rounded
: Icons.visibility_rounded, : Icons.visibility_rounded,
color: Colors.white70, color: Colors.white70,
@ -240,11 +214,21 @@ class _LoginPageState extends State<LoginPage> {
child: const Text('Forgot password?'), 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( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: FilledButton(
onPressed: _isSigningIn ? null : _submit, onPressed: state.isSubmitting ? null : _submit,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: colorScheme.primary, backgroundColor: colorScheme.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@ -253,7 +237,7 @@ class _LoginPageState extends State<LoginPage> {
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
), ),
), ),
child: _isSigningIn child: state.isSubmitting
? const SizedBox( ? const SizedBox(
height: 20, height: 20,
width: 20, width: 20,
@ -288,21 +272,23 @@ class _LoginPageState extends State<LoginPage> {
class _AuthTextField extends StatelessWidget { class _AuthTextField extends StatelessWidget {
const _AuthTextField({ const _AuthTextField({
required this.controller, required this.initialValue,
required this.label, required this.label,
required this.hintText, required this.hintText,
required this.prefixIcon, required this.prefixIcon,
required this.validator, required this.validator,
required this.onChanged,
this.keyboardType, this.keyboardType,
this.obscureText = false, this.obscureText = false,
this.suffixIcon, this.suffixIcon,
}); });
final TextEditingController controller; final String initialValue;
final String label; final String label;
final String hintText; final String hintText;
final IconData prefixIcon; final IconData prefixIcon;
final String? Function(String?) validator; final String? Function(String?) validator;
final ValueChanged<String> onChanged;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final bool obscureText; final bool obscureText;
final Widget? suffixIcon; final Widget? suffixIcon;
@ -310,10 +296,11 @@ class _AuthTextField extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFormField( return TextFormField(
controller: controller, initialValue: initialValue,
keyboardType: keyboardType, keyboardType: keyboardType,
obscureText: obscureText, obscureText: obscureText,
validator: validator, validator: validator,
onChanged: onChanged,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../manager/history_ui_state.dart'; import '../viewmodels/history_ui_state.dart';
import '../manager/history_view_model.dart'; import '../viewmodels/history_view_model.dart';
import '../widgets/history_section_card.dart'; import '../widgets/history_section_card.dart';
class HistoryPage extends ConsumerWidget { class HistoryView extends ConsumerWidget {
const HistoryPage({super.key}); const HistoryView({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../manager/history_ui_state.dart'; import '../viewmodels/history_ui_state.dart';
import 'history_item_tile.dart'; import 'history_item_tile.dart';
class HistorySectionCard extends StatelessWidget { class HistorySectionCard extends StatelessWidget {
@ -57,7 +57,7 @@ class HistorySectionCard extends StatelessWidget {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
'${section.totalTransactionsLabel} ${section.totalAmountLabel}', '${section.totalTransactionsLabel} | ${section.totalAmountLabel}',
style: textTheme.labelMedium?.copyWith( style: textTheme.labelMedium?.copyWith(
color: colorScheme.onPrimary.withOpacity(0.9), color: colorScheme.onPrimary.withOpacity(0.9),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,

View File

@ -28,4 +28,3 @@ class HomeLocalDataSourceImpl implements HomeLocalDataSource {
); );
} }
} }

View File

@ -1,9 +1,5 @@
import '../../domain/entities/recent_scan.dart'; import '../../domain/entities/recent_scan.dart';
class RecentScanModel extends RecentScan { class RecentScanModel extends RecentScan {
const RecentScanModel({ const RecentScanModel({required super.title, required super.subtitle});
required super.title,
required super.subtitle,
});
} }

View File

@ -18,4 +18,3 @@ class HomeRepositoryImpl implements HomeRepository {
); );
} }
} }

View File

@ -1,12 +1,8 @@
import 'recent_scan.dart'; import 'recent_scan.dart';
class HomeContent { class HomeContent {
const HomeContent({ const HomeContent({required this.carouselAssets, required this.recentScans});
required this.carouselAssets,
required this.recentScans,
});
final List<String> carouselAssets; final List<String> carouselAssets;
final List<RecentScan> recentScans; final List<RecentScan> recentScans;
} }

View File

@ -1,10 +1,6 @@
class RecentScan { class RecentScan {
const RecentScan({ const RecentScan({required this.title, required this.subtitle});
required this.title,
required this.subtitle,
});
final String title; final String title;
final String subtitle; final String subtitle;
} }

View File

@ -3,4 +3,3 @@ import '../entities/home_content.dart';
abstract class HomeRepository { abstract class HomeRepository {
Future<HomeContent> getHomeContent({int recentLimit = 5}); Future<HomeContent> getHomeContent({int recentLimit = 5});
} }

View File

@ -10,4 +10,3 @@ class GetHomeContent {
return _repository.getHomeContent(recentLimit: recentLimit); return _repository.getHomeContent(recentLimit: recentLimit);
} }
} }

View File

@ -42,11 +42,7 @@ class HomeViewModel extends AsyncNotifier<HomeUiState> {
); );
} }
void onSeeAllTapped() { void onSeeAllTapped() {}
// TODO: navigate to full list
}
void onRecentScanTapped(int index) { void onRecentScanTapped(int index) {}
// TODO: navigate to details
}
} }

View File

@ -1,11 +1,11 @@
import 'package:carousel_slider/carousel_slider.dart'; 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:cb_prestige_qr/features/history/presentation/widgets/history_item_tile.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../../../core/presentation/widgets/global_loading_overlay.dart'; import '../viewmodels/home_ui_state.dart';
import '../manager/home_ui_state.dart'; import '../viewmodels/home_view_model.dart';
import '../manager/home_view_model.dart';
import '../widgets/home_today_summary_section.dart'; import '../widgets/home_today_summary_section.dart';
class HomeView extends ConsumerWidget { class HomeView extends ConsumerWidget {
@ -39,17 +39,17 @@ class HomeView extends ConsumerWidget {
], ],
), ),
), ),
body: stateAsync.when( body: stateAsync.when(
data: (state) => _HomeBody( data: (state) => _HomeBody(
state: state, state: state,
onSeeAll: viewModel.onSeeAllTapped, onSeeAll: viewModel.onSeeAllTapped,
onRecentScanTap: viewModel.onRecentScanTapped, onRecentScanTap: viewModel.onRecentScanTapped,
), ),
loading: () => const GlobalLoadingOverlay(isLoading: true), loading: () => const GlobalLoadingOverlay(isLoading: true),
error: (error, stackTrace) => Center( error: (error, stackTrace) => Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text('Failed to load Home'), const Text('Failed to load Home'),
@ -80,8 +80,7 @@ class _HomeBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final shellBottomInset = final shellBottomInset = 70 + MediaQuery.of(context).padding.bottom;
70 + MediaQuery.of(context).padding.bottom;
final contentBottomPadding = shellBottomInset + 8; final contentBottomPadding = shellBottomInset + 8;
return CustomScrollView( return CustomScrollView(

View File

@ -84,7 +84,6 @@ class HomeContextSection extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
hasCustomer hasCustomer

View File

@ -164,4 +164,3 @@ class _MiniStat extends StatelessWidget {
); );
} }
} }

View File

@ -1,8 +1,5 @@
import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart'; import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart';
abstract interface class ScanRemoteDataSource { abstract interface class ScanRemoteDataSource {
Future<Map<String, dynamic>> submitScan({ Future<Map<String, dynamic>> submitScan({required ScanSubmitPayload payload});
required ScanSubmitPayload payload,
});
} }

View File

@ -12,4 +12,3 @@ class FakeScanRemoteDataSource implements ScanRemoteDataSource {
}; };
} }
} }

View File

@ -47,9 +47,7 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource {
throw const FormatException('QR scan response must be a JSON object.'); throw const FormatException('QR scan response must be a JSON object.');
} }
final data = decoded.map( final data = decoded.map((key, value) => MapEntry(key.toString(), value));
(key, value) => MapEntry(key.toString(), value),
);
final isOk = data['OK'] == true || data['ok'] == true; final isOk = data['OK'] == true || data['ok'] == true;
if (!isOk) { if (!isOk) {
throw HttpException( throw HttpException(
@ -78,7 +76,9 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource {
String _buildStatusMessage(int statusCode, String responseBody) { String _buildStatusMessage(int statusCode, String responseBody) {
final normalizedBody = responseBody.trim(); final normalizedBody = responseBody.trim();
final bodySuffix = normalizedBody.isEmpty ? '' : ' Response: $normalizedBody'; final bodySuffix = normalizedBody.isEmpty
? ''
: ' Response: $normalizedBody';
return switch (statusCode) { return switch (statusCode) {
HttpStatus.badRequest => HttpStatus.badRequest =>
@ -91,8 +91,7 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource {
'Submit failed with status 404 (Not Found).$bodySuffix', 'Submit failed with status 404 (Not Found).$bodySuffix',
HttpStatus.internalServerError => HttpStatus.internalServerError =>
'Submit failed with status 500 (Server Error).$bodySuffix', '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',
}; };
} }
} }

View File

@ -1,10 +1,7 @@
import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart';
class ScannedQrModel { class ScannedQrModel {
const ScannedQrModel({ const ScannedQrModel({required this.rawValue, required this.scannedAt});
required this.rawValue,
required this.scannedAt,
});
factory ScannedQrModel.fromJson(Map<String, dynamic> json) { factory ScannedQrModel.fromJson(Map<String, dynamic> json) {
return ScannedQrModel( return ScannedQrModel(
@ -27,4 +24,3 @@ class ScannedQrModel {
return ScannedQr(rawValue: rawValue, scannedAt: scannedAt); return ScannedQr(rawValue: rawValue, scannedAt: scannedAt);
} }
} }

View File

@ -7,9 +7,7 @@ class ScanSubmitPayload {
required this.scannedByDeviceId, required this.scannedByDeviceId,
}); });
factory ScanSubmitPayload.fromRawValue({ factory ScanSubmitPayload.fromRawValue({required String rawValue}) {
required String rawValue,
}) {
final payload = ScanSubmitPayload( final payload = ScanSubmitPayload(
token: rawValue.trim(), token: rawValue.trim(),
merchantId: 'merchant-001', merchantId: 'merchant-001',

View File

@ -1,10 +1,6 @@
class ScannedQr { class ScannedQr {
const ScannedQr({ const ScannedQr({required this.rawValue, required this.scannedAt});
required this.rawValue,
required this.scannedAt,
});
final String rawValue; final String rawValue;
final DateTime scannedAt; final DateTime scannedAt;
} }

View File

@ -10,4 +10,3 @@ class ProcessScanUseCase {
return _repository.processRawValue(rawValue); return _repository.processRawValue(rawValue);
} }
} }

View File

@ -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<ScanController, ScanState> {
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<ScanState>(value),
);
}
}
String _$scanControllerHash() => r'5984074ff43ae1904e2025b355a689580d402a09';
abstract class _$ScanController extends $Notifier<ScanState> {
ScanState build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<ScanState, ScanState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ScanState, ScanState>,
ScanState,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View File

@ -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/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/repositories/scan_repository.dart';
import 'package:cb_prestige_qr/features/scan/domain/use_cases/process_scan_use_case.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<ScanRemoteDataSource>(
(ref) => HttpScanRemoteDataSource(),
);
@riverpod final scanRepositoryProvider = Provider<ScanRepository>(
ScanRemoteDataSource scanRemoteDataSource(Ref ref) { (ref) => ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider)),
return HttpScanRemoteDataSource(); );
}
@riverpod
ScanRepository scanRepository(Ref ref) {
return ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider));
}
@riverpod
ProcessScanUseCase processScanUseCase(Ref ref) {
return ProcessScanUseCase(ref.watch(scanRepositoryProvider));
}
final processScanUseCaseProvider = Provider<ProcessScanUseCase>(
(ref) => ProcessScanUseCase(ref.watch(scanRepositoryProvider)),
);

View File

@ -1,7 +1,9 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:cb_prestige_qr/features/scan/presentation/providers/scan_providers.dart';
import 'package:cb_prestige_qr/features/scan/scan_providers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
part 'scan_controller.g.dart'; final scanControllerProvider = NotifierProvider<ScanController, ScanState>(
ScanController.new,
);
class ScanState { class ScanState {
const ScanState({ const ScanState({
@ -41,8 +43,7 @@ class ScanState {
} }
} }
@riverpod class ScanController extends Notifier<ScanState> {
class ScanController extends _$ScanController {
@override @override
ScanState build() => const ScanState(); ScanState build() => const ScanState();
@ -60,12 +61,12 @@ class ScanController extends _$ScanController {
isProcessing: false, isProcessing: false,
scannedValue: result.rawValue, scannedValue: result.rawValue,
); );
} catch (e) { } catch (error) {
state = state.copyWith( state = state.copyWith(
isProcessing: false, isProcessing: false,
isScanning: true, isScanning: true,
scannedValue: null, scannedValue: null,
errorMessage: e.toString(), errorMessage: error.toString(),
); );
} }
} }
@ -81,8 +82,11 @@ class ScanController extends _$ScanController {
scannedValue: null, scannedValue: null,
errorMessage: null, errorMessage: null,
); );
} catch (e) { } catch (error) {
state = state.copyWith(isSubmitting: false, errorMessage: e.toString()); state = state.copyWith(
isSubmitting: false,
errorMessage: error.toString(),
);
rethrow; rethrow;
} }
} }

View File

@ -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'; import 'package:flutter/material.dart';
class ScanFlow extends StatelessWidget { class ScanFlow extends StatelessWidget {

View File

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:cb_prestige_qr/core/presentation/widgets/start_loading_overlay.dart'; 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/viewmodels/scan_controller.dart';
import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_result_page.dart'; import 'package:cb_prestige_qr/features/scan/presentation/views/scan_result_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';

View File

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cb_prestige_qr/core/utils/MainShell.dart'; import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart'; import 'package:cb_prestige_qr/features/scan/presentation/viewmodels/scan_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:jwt_decoder/jwt_decoder.dart';
@ -150,9 +150,7 @@ class ScanResultPage extends ConsumerWidget {
content: Text('QR submitted successfully.'), content: Text('QR submitted successfully.'),
), ),
); );
ref ref.read(navIndexProvider.notifier).setIndex(0);
.read(navIndexNotifierProvider.notifier)
.setIndex(0);
ref ref
.read(scanControllerProvider.notifier) .read(scanControllerProvider.notifier)
.resumeScanning(); .resumeScanning();
@ -260,10 +258,17 @@ Map<String, dynamic>? _tryDecodeFromUri(String value) {
return null; return null;
} }
Map<String, dynamic>? _tryDecodeBase64Json(String value, {required bool urlSafe}) { Map<String, dynamic>? _tryDecodeBase64Json(
String value, {
required bool urlSafe,
}) {
try { try {
final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value); final normalized = urlSafe
final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized); ? base64Url.normalize(value)
: base64.normalize(value);
final bytes = urlSafe
? base64Url.decode(normalized)
: base64.decode(normalized);
final decoded = utf8.decode(bytes); final decoded = utf8.decode(bytes);
final payload = jsonDecode(decoded); final payload = jsonDecode(decoded);
if (payload is! Map) return null; if (payload is! Map) return null;
@ -275,8 +280,12 @@ Map<String, dynamic>? _tryDecodeBase64Json(String value, {required bool urlSafe}
String? _tryDecodeBase64Text(String value, {required bool urlSafe}) { String? _tryDecodeBase64Text(String value, {required bool urlSafe}) {
try { try {
final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value); final normalized = urlSafe
final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized); ? base64Url.normalize(value)
: base64.normalize(value);
final bytes = urlSafe
? base64Url.decode(normalized)
: base64.decode(normalized);
final decoded = utf8.decode(bytes).trim(); final decoded = utf8.decode(bytes).trim();
if (decoded.isEmpty || decoded == value) return null; if (decoded.isEmpty || decoded == value) return null;
return decoded; return decoded;

View File

@ -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<ScanRemoteDataSource> {
ScanRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'scanRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$scanRemoteDataSourceHash();
@$internal
@override
$ProviderElement<ScanRemoteDataSource> $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<ScanRemoteDataSource>(value),
);
}
}
String _$scanRemoteDataSourceHash() =>
r'f25d69a350b64c4dd0cf8ef124838bb351b5e4a1';
@ProviderFor(scanRepository)
final scanRepositoryProvider = ScanRepositoryProvider._();
final class ScanRepositoryProvider
extends $FunctionalProvider<ScanRepository, ScanRepository, ScanRepository>
with $Provider<ScanRepository> {
ScanRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'scanRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$scanRepositoryHash();
@$internal
@override
$ProviderElement<ScanRepository> $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<ScanRepository>(value),
);
}
}
String _$scanRepositoryHash() => r'3e719b962dbb1014143710238128234371ef141e';
@ProviderFor(processScanUseCase)
final processScanUseCaseProvider = ProcessScanUseCaseProvider._();
final class ProcessScanUseCaseProvider
extends
$FunctionalProvider<
ProcessScanUseCase,
ProcessScanUseCase,
ProcessScanUseCase
>
with $Provider<ProcessScanUseCase> {
ProcessScanUseCaseProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'processScanUseCaseProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$processScanUseCaseHash();
@$internal
@override
$ProviderElement<ProcessScanUseCase> $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<ProcessScanUseCase>(value),
);
}
}
String _$processScanUseCaseHash() =>
r'ce9f6a3d0de27fe0f7f8e7fa1ac9c1f88ad57e43';

View File

@ -1,5 +1,6 @@
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart'; 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'; import '../../domain/entities/settings_content.dart';
@ -10,32 +11,46 @@ abstract class SettingsLocalDataSource {
} }
class SettingsLocalDataSourceImpl implements SettingsLocalDataSource { class SettingsLocalDataSourceImpl implements SettingsLocalDataSource {
SettingsLocalDataSourceImpl(this._storage); SettingsLocalDataSourceImpl(this._storage, this._authLocalDataSource);
final LocalStorageService _storage; final LocalStorageService _storage;
final AuthLocalDataSource _authLocalDataSource;
bool _notificationsEnabled = true; static const _notificationsKey = 'settings_notifications_enabled';
bool _hapticsEnabled = true; static const _hapticsKey = 'settings_haptics_enabled';
@override @override
Future<SettingsContent> getSettings() async { Future<SettingsContent> getSettings() async {
final userProfile = await UserProfileRepository(_storage).load(); final userProfile = await _authLocalDataSource.getSavedUserProfile();
return SettingsContent( return SettingsContent(
notificationsEnabled: _notificationsEnabled, notificationsEnabled: await _storage.getBool(_notificationsKey) ?? true,
hapticsEnabled: _hapticsEnabled, hapticsEnabled: await _storage.getBool(_hapticsKey) ?? true,
appVersionLabel: 'v1.0.0', appVersionLabel: 'v1.0.0',
userProfile: userProfile, userProfile: userProfile ?? const _FallbackUserProfileFactory().create(),
); );
} }
@override @override
Future<void> setNotificationsEnabled(bool value) async { Future<void> setNotificationsEnabled(bool value) async {
_notificationsEnabled = value; await _storage.setBool(_notificationsKey, value);
} }
@override @override
Future<void> setHapticsEnabled(bool value) async { Future<void> 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',
);
} }
} }

View File

@ -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 { class SettingsContent {
const SettingsContent({ const SettingsContent({

View File

@ -1,7 +1,6 @@
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../../auth/domain/user_profile.dart';
@immutable @immutable
class SettingsUiState { class SettingsUiState {
const SettingsUiState({ const SettingsUiState({

View File

@ -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 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../../../core/storage/local_storage_service.dart';
import '../../data/data_sources/settings_local_data_source.dart'; import '../../data/data_sources/settings_local_data_source.dart';
import '../../data/repositories/settings_repository_impl.dart'; import '../../data/repositories/settings_repository_impl.dart';
import '../../domain/entities/settings_content.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 '../../domain/use_cases/set_notifications_enabled.dart';
import 'settings_ui_state.dart'; import 'settings_ui_state.dart';
final _localStorageServiceProvider = Provider<LocalStorageService>(
(ref) => LocalStorageService(),
);
final _settingsLocalDataSourceProvider = Provider<SettingsLocalDataSource>( final _settingsLocalDataSourceProvider = Provider<SettingsLocalDataSource>(
(ref) => SettingsLocalDataSourceImpl(ref.watch(_localStorageServiceProvider)), (ref) => SettingsLocalDataSourceImpl(
ref.watch(localStorageServiceProvider),
ref.watch(authLocalDataSourceProvider),
),
); );
final _settingsRepositoryProvider = Provider<SettingsRepository>( final _settingsRepositoryProvider = Provider<SettingsRepository>(
@ -56,12 +56,16 @@ class SettingsViewModel extends AsyncNotifier<SettingsUiState> {
} }
Future<void> toggleNotifications(bool value) async { Future<void> toggleNotifications(bool value) async {
await ref.watch(_setNotificationsEnabledProvider)(value); await ref.read(_setNotificationsEnabledProvider)(value);
state = state.whenData((s) => s.copyWith(notificationsEnabled: value)); state = state.whenData((s) => s.copyWith(notificationsEnabled: value));
} }
Future<void> toggleHaptics(bool value) async { Future<void> toggleHaptics(bool value) async {
await ref.watch(_setHapticsEnabledProvider)(value); await ref.read(_setHapticsEnabledProvider)(value);
state = state.whenData((s) => s.copyWith(hapticsEnabled: value)); state = state.whenData((s) => s.copyWith(hapticsEnabled: value));
} }
Future<void> logout() async {
await ref.read(logoutUseCaseProvider)();
}
} }

View File

@ -1,14 +1,14 @@
import 'package:cb_prestige_qr/core/utils/MainShell.dart'; import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
import 'package:cb_prestige_qr/features/auth/presentation/pages/login_page.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:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../../auth/domain/user_profile.dart'; import '../viewmodels/settings_ui_state.dart';
import '../manager/settings_ui_state.dart'; import '../viewmodels/settings_view_model.dart';
import '../manager/settings_view_model.dart';
class SettingsPage extends ConsumerWidget { class SettingsView extends ConsumerWidget {
const SettingsPage({super.key}); const SettingsView({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -31,11 +31,14 @@ class SettingsPage extends ConsumerWidget {
onNotificationsChanged: viewModel.toggleNotifications, onNotificationsChanged: viewModel.toggleNotifications,
onHapticsChanged: viewModel.toggleHaptics, onHapticsChanged: viewModel.toggleHaptics,
onLogout: () { onLogout: () {
ref.read(navIndexNotifierProvider.notifier).setIndex(0); viewModel.logout().then((_) {
Navigator.of(context).pushAndRemoveUntil( if (!context.mounted) return;
MaterialPageRoute(builder: (_) => const LoginPage()), ref.read(navIndexProvider.notifier).setIndex(0);
(route) => false, Navigator.of(context).pushAndRemoveUntil(
); MaterialPageRoute(builder: (_) => const LoginView()),
(route) => false,
);
});
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),

View File

@ -1,6 +1,6 @@
import 'dart:async'; 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'; import 'package:flutter/material.dart';
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
@ -26,9 +26,9 @@ class _SplashScreenState extends State<SplashScreen> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_timer = Timer(widget.duration, () { _timer = Timer(widget.duration, () {
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pushReplacement( Navigator.of(
MaterialPageRoute(builder: (_) => const MainShell()), context,
); ).pushReplacement(MaterialPageRoute(builder: (_) => const MainShell()));
}); });
}); });
} }
@ -43,12 +43,8 @@ class _SplashScreenState extends State<SplashScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SizedBox.expand( body: SizedBox.expand(
child: Image.asset( child: Image.asset(widget.imageAssetPath, fit: BoxFit.cover),
widget.imageAssetPath,
fit: BoxFit.cover,
),
), ),
); );
} }
} }

View File

@ -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:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -6,56 +6,3 @@ void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: MyApp())); 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(),
);
}
}

View File

@ -217,6 +217,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -287,7 +303,7 @@ packages:
source: hosted source: hosted
version: "3.3.1" version: "3.3.1"
flutter_svg: flutter_svg:
dependency: "direct dev" dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"

View File

@ -42,6 +42,8 @@ dependencies:
mobile_scanner: ^7.2.0 mobile_scanner: ^7.2.0
shared_preferences: ^2.5.5 shared_preferences: ^2.5.5
jwt_decoder: ^2.0.1 jwt_decoder: ^2.0.1
dio: ^5.9.0
flutter_svg: ^2.2.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -55,7 +57,6 @@ dev_dependencies:
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
riverpod_generator: ^4.0.3 riverpod_generator: ^4.0.3
build_runner: ^2.13.1 build_runner: ^2.13.1
flutter_svg: ^2.2.4
flutter_native_splash: 2.4.7 flutter_native_splash: 2.4.7

View File

@ -5,17 +5,16 @@
// gestures. You can also use WidgetTester to find child widgets in the widget // 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. // tree, read text, and verify that the values of widget properties are correct.
import 'package:cb_prestige_qr/app/app.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:cb_prestige_qr/main.dart';
void main() { void main() {
testWidgets('shows login form fields', (WidgetTester tester) async { 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('Secure Login'), findsOneWidget);
expect(find.text('Phone Number'), findsOneWidget); expect(find.text('Username'), findsOneWidget);
expect(find.text('User ID (Username)'), findsOneWidget);
expect(find.text('Password'), findsOneWidget); expect(find.text('Password'), findsOneWidget);
expect(find.text('Sign In'), findsOneWidget); expect(find.text('Sign In'), findsOneWidget);
}); });