From 3e88aad43fd6ed5713fca85977df8acd4ad004b1 Mon Sep 17 00:00:00 2001 From: asif Date: Sun, 9 Nov 2025 15:42:50 +0530 Subject: [PATCH] T&C #1 --- flutter_01.png | Bin 0 -> 10594 bytes lib/api/services/auth_service.dart | 23 +++- lib/config/routes.dart | 4 +- lib/data/repositories/auth_repository.dart | 22 +++- lib/di/injection.dart | 2 +- lib/features/auth/controllers/auth_cubit.dart | 50 +++++++- lib/features/auth/controllers/auth_state.dart | 36 ++++-- lib/features/auth/models/auth_token.dart | 42 +++++- lib/features/auth/screens/login_screen.dart | 120 +++++++----------- .../auth/screens/tnc_required_screen.dart | 39 ++++++ lib/widgets/tnc_dialog.dart | 92 ++++++++++++++ 11 files changed, 332 insertions(+), 98 deletions(-) create mode 100644 flutter_01.png create mode 100644 lib/features/auth/screens/tnc_required_screen.dart create mode 100644 lib/widgets/tnc_dialog.dart diff --git a/flutter_01.png b/flutter_01.png new file mode 100644 index 0000000000000000000000000000000000000000..11e231478e4bee05c462c71922b619bd1a4110ba GIT binary patch literal 10594 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 Q^D`G-sp+aBmF+(K7gJY$AOHXW literal 0 HcmV?d00001 diff --git a/lib/api/services/auth_service.dart b/lib/api/services/auth_service.dart index 1486645..333cd42 100644 --- a/lib/api/services/auth_service.dart +++ b/lib/api/services/auth_service.dart @@ -141,4 +141,25 @@ class AuthService { } return; } -} + + Future setTncflag() async{ + try { + final response = await _dio.post( + '/api/auth/tnc', + data: {"flag": true}, + ); + if (response.statusCode != 200) { + throw AuthException('Failed to proceed with T&C'); + } + } + on DioException catch (e) { + if (kDebugMode) { + print(e.toString()); + } + throw NetworkException('Network error during T&C Setup'); + } catch (e) { + throw UnexpectedException( + 'Unexpected error: ${e.toString()}'); + } + } +} \ No newline at end of file diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 728a57d..a9e16b5 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -10,6 +10,7 @@ import '../features/dashboard/screens/dashboard_screen.dart'; // import '../features/transactions/screens/transactions_screen.dart'; // import '../features/payments/screens/payments_screen.dart'; // import '../features/settings/screens/settings_screen.dart'; + import 'package:kmobile/features/auth/screens/tnc_required_screen.dart'; class AppRoutes { // Private constructor to prevent instantiation @@ -34,7 +35,8 @@ class AppRoutes { return MaterialPageRoute(builder: (_) => const SplashScreen()); case login: return MaterialPageRoute(builder: (_) => const LoginScreen()); - + case TncRequiredScreen.routeName: // Renamed class + return MaterialPageRoute(builder: (_) => const TncRequiredScreen()); // Renamed class case mPin: return MaterialPageRoute( builder: (_) => const MPinScreen( diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index 9a96026..860d92b 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -13,10 +13,11 @@ class AuthRepository { static const _accessTokenKey = 'access_token'; static const _tokenExpiryKey = 'token_expiry'; + static const _tncKey = 'tnc'; AuthRepository(this._authService, this._userService, this._secureStorage); - Future> login(String customerNo, String password) async { + Future<(List, AuthToken)> login(String customerNo, String password) async { // Create credentials and call service final credentials = AuthCredentials(customerNo: customerNo, password: password); @@ -27,7 +28,7 @@ class AuthRepository { // Get and save user profile final users = await _userService.getUserDetails(); - return users; + return (users, authToken); } Future isLoggedIn() async { @@ -47,6 +48,7 @@ class AuthRepository { await _secureStorage.write(_accessTokenKey, token.accessToken); await _secureStorage.write( _tokenExpiryKey, token.expiresAt.toIso8601String()); + await _secureStorage.write(_tncKey, token.tnc.toString()); } Future clearAuthTokens() async { @@ -56,13 +58,27 @@ class AuthRepository { Future _getAuthToken() async { final accessToken = await _secureStorage.read(_accessTokenKey); final expiryString = await _secureStorage.read(_tokenExpiryKey); + final tncString = await _secureStorage.read(_tncKey); if (accessToken != null && expiryString != null) { - return AuthToken( + final authToken = AuthToken( accessToken: accessToken, expiresAt: DateTime.parse(expiryString), + tnc: tncString == 'true', // Parse 'true' string to true, otherwise false ); + return authToken; } return null; } + + Future acceptTnc() async { + // This method calls the setTncFlag function + try { + await _authService.setTncflag(); + } catch (e) { + // Handle or rethrow the error as needed + print('Error setting TNC flag: $e'); + rethrow; + } + } } diff --git a/lib/di/injection.dart b/lib/di/injection.dart index d28baa4..ca5016e 100644 --- a/lib/di/injection.dart +++ b/lib/di/injection.dart @@ -62,7 +62,7 @@ Future setupDependencies() async { // Register controllers/cubits getIt.registerFactory( - () => AuthCubit(getIt(), getIt())); + () => AuthCubit(getIt(), getIt(), getIt())); } Dio _createDioClient() { diff --git a/lib/features/auth/controllers/auth_cubit.dart b/lib/features/auth/controllers/auth_cubit.dart index 0140a9c..1195548 100644 --- a/lib/features/auth/controllers/auth_cubit.dart +++ b/lib/features/auth/controllers/auth_cubit.dart @@ -1,14 +1,20 @@ import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; import 'package:kmobile/api/services/user_service.dart'; import 'package:kmobile/core/errors/exceptions.dart'; +import 'package:kmobile/data/models/user.dart'; +import 'package:kmobile/features/auth/models/auth_token.dart'; +import 'package:kmobile/security/secure_storage.dart'; import '../../../data/repositories/auth_repository.dart'; import 'auth_state.dart'; class AuthCubit extends Cubit { final AuthRepository _authRepository; final UserService _userService; + final SecureStorage _secureStorage; - AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) { + AuthCubit(this._authRepository, this._userService, this._secureStorage) + : super(AuthInitial()) { checkAuthStatus(); } @@ -29,22 +35,56 @@ class AuthCubit extends Cubit { Future refreshUserData() async { try { - // emit(AuthLoading()); final users = await _userService.getUserDetails(); emit(Authenticated(users)); } catch (e) { emit(AuthError('Failed to refresh user data: ${e.toString()}')); - // Optionally, re-emit the previous state or handle as needed } } Future login(String customerNo, String password) async { emit(AuthLoading()); try { - final users = await _authRepository.login(customerNo, password); - emit(Authenticated(users)); + final (users, authToken) = await _authRepository.login(customerNo, password); + + if (authToken.tnc == false) { + // TNC not accepted, tell UI to show the dialog + emit(ShowTncDialog(authToken, users)); + } else { + // TNC already accepted, emit Authenticated and then proceed to MPIN check + emit(Authenticated(users)); + await _checkMpinAndNavigate(); + } } catch (e) { emit(AuthError(e is AuthException ? e.message : e.toString())); } } + + Future onTncDialogResult( + bool agreed, AuthToken authToken, List users) async { + if (agreed) { + try { + await _authRepository.acceptTnc(); + // User agreed, emit Authenticated and then proceed to MPIN check + emit(Authenticated(users)); + await _checkMpinAndNavigate(); + } catch (e) { + emit(AuthError('Failed to accept TNC: $e')); + } + } else { + // User disagreed, tell UI to navigate to the required screen + emit(NavigateToTncRequiredScreen()); + } + } + + Future _checkMpinAndNavigate() async { + final mpin = await _secureStorage.read('mpin'); + if (mpin == null) { + // No MPIN, tell UI to navigate to MPIN setup + emit(NavigateToMpinSetupScreen()); + } else { + // MPIN exists, tell UI to navigate to the dashboard + emit(NavigateToDashboardScreen()); + } + } } diff --git a/lib/features/auth/controllers/auth_state.dart b/lib/features/auth/controllers/auth_state.dart index abed153..f9fbec7 100644 --- a/lib/features/auth/controllers/auth_state.dart +++ b/lib/features/auth/controllers/auth_state.dart @@ -1,9 +1,12 @@ import 'package:equatable/equatable.dart'; -import '../../../data/models/user.dart'; +import 'package:kmobile/data/models/user.dart'; +import 'package:kmobile/features/auth/models/auth_token.dart'; abstract class AuthState extends Equatable { + const AuthState(); + @override - List get props => []; + List get props => []; } class AuthInitial extends AuthState {} @@ -12,20 +15,37 @@ class AuthLoading extends AuthState {} class Authenticated extends AuthState { final List users; - - Authenticated(this.users); + const Authenticated(this.users); @override - List get props => [users]; + List get props => [users]; } class Unauthenticated extends AuthState {} class AuthError extends AuthState { final String message; - - AuthError(this.message); + const AuthError(this.message); @override - List get props => [message]; + List get props => [message]; } + +// --- New States for Navigation and Dialog --- + +// State to indicate that the TNC dialog needs to be shown +class ShowTncDialog extends AuthState { + final AuthToken authToken; + final List users; + const ShowTncDialog(this.authToken, this.users); + + @override + List get props => [authToken, users]; +} + +// States to trigger specific navigations from the UI +class NavigateToTncRequiredScreen extends AuthState {} + +class NavigateToMpinSetupScreen extends AuthState {} + +class NavigateToDashboardScreen extends AuthState {} \ No newline at end of file diff --git a/lib/features/auth/models/auth_token.dart b/lib/features/auth/models/auth_token.dart index ad1c56d..586dc49 100644 --- a/lib/features/auth/models/auth_token.dart +++ b/lib/features/auth/models/auth_token.dart @@ -6,18 +6,22 @@ import 'package:equatable/equatable.dart'; class AuthToken extends Equatable { final String accessToken; final DateTime expiresAt; + final bool tnc; const AuthToken({ required this.accessToken, required this.expiresAt, + required this.tnc, }); - factory AuthToken.fromJson(Map json) { - return AuthToken( - accessToken: json['token'], - expiresAt: _decodeExpiryFromToken(json['token']), - ); - } + factory AuthToken.fromJson(Map json) { + final token = json['token']; + return AuthToken( + accessToken: token, + expiresAt: _decodeExpiryFromToken(token), // Keep existing method for expiry + tnc: _decodeTncFromToken(token), // Use new method for tnc + ); + } static DateTime _decodeExpiryFromToken(String token) { try { @@ -41,9 +45,33 @@ class AuthToken extends Equatable { return DateTime.now().add(const Duration(hours: 1)); } } + + static bool _decodeTncFromToken(String token) { + try { + final parts = token.split('.'); + if (parts.length != 3) { + throw Exception('Invalid JWT format for TNC decoding'); + } + final payload = parts[1]; + String normalized = base64Url.normalize(payload); + final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized))); + + if (payloadMap is! Map || !payloadMap.containsKey('tnc')) { + // If 'tnc' is not present in the payload, default to false + return false; + } + + // Assuming 'tnc' is directly a boolean in the JWT payload + return payloadMap['tnc'] as bool; + } catch (e) { + log('Error decoding tnc from token: $e'); + // Default to false if decoding fails or 'tnc' is not found/invalid + return false; + } +} bool get isExpired => DateTime.now().isAfter(expiresAt); @override - List get props => [accessToken, expiresAt]; + List get props => [accessToken, expiresAt, tnc]; } diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart index ccad012..9ab4aa8 100644 --- a/lib/features/auth/screens/login_screen.dart +++ b/lib/features/auth/screens/login_screen.dart @@ -1,12 +1,11 @@ -import '../../../l10n/app_localizations.dart'; - -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:kmobile/di/injection.dart'; +import 'package:kmobile/app.dart'; import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/set_password_screen.dart'; -import 'package:kmobile/security/secure_storage.dart'; -import '../../../app.dart'; +import 'package:kmobile/features/auth/screens/tnc_required_screen.dart'; +import 'package:kmobile/widgets/tnc_dialog.dart'; +import '../../../l10n/app_localizations.dart'; +import 'package:flutter/material.dart'; import '../controllers/auth_cubit.dart'; import '../controllers/auth_state.dart'; @@ -23,7 +22,6 @@ class LoginScreenState extends State final _customerNumberController = TextEditingController(); final _passwordController = TextEditingController(); bool _obscurePassword = true; - //bool _showWelcome = true; @override void dispose() { @@ -44,36 +42,51 @@ class LoginScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - // appBar: AppBar(title: const Text('Login')), body: BlocConsumer( listener: (context, state) async { - if (state is Authenticated) { - final storage = getIt(); - final mpin = await storage.read('mpin'); - if (!context.mounted) return; - if (mpin == null) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => MPinScreen( - mode: MPinMode.set, - onCompleted: (_) { - Navigator.of( - context, - rootNavigator: true, - ).pushReplacement( - MaterialPageRoute( - builder: (_) => const NavigationScaffold(), - ), - ); - }, - ), - ), - ); - } else { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const NavigationScaffold()), - ); + if (state is ShowTncDialog) { + // The dialog now returns a boolean for the 'disagree' case, + // or it completes when the 'proceed' action is finished. + final agreed = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => TncDialog( + onProceed: () async { + // This function is passed to the dialog. + // It calls the cubit and completes when the cubit's work is done. + await context + .read() + .onTncDialogResult(true, state.authToken, state.users); + }, + ), + ); + + // If 'agreed' is false, it means the user clicked 'Disagree'. + if (agreed == false) { + if (!context.mounted) return; + context + .read() + .onTncDialogResult(false, state.authToken, state.users); } + } else if (state is NavigateToTncRequiredScreen) { + Navigator.of(context).pushNamed(TncRequiredScreen.routeName); + } else if (state is NavigateToMpinSetupScreen) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => MPinScreen( + mode: MPinMode.set, + onCompleted: (_) { + Navigator.of(context, rootNavigator: true).pushReplacement( + MaterialPageRoute(builder: (_) => const NavigationScaffold()), + ); + }, + ), + ), + ); + } else if (state is NavigateToDashboardScreen) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const NavigationScaffold()), + ); } else if (state is AuthError) { if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') { Navigator.of(context).push(MaterialPageRoute( @@ -87,6 +100,7 @@ class LoginScreenState extends State } }, builder: (context, state) { + // The commented out section is removed for clarity, the logic is now above. return Padding( padding: const EdgeInsets.all(24.0), child: Form( @@ -107,7 +121,6 @@ class LoginScreenState extends State }, ), const SizedBox(height: 16), - // Title Text( AppLocalizations.of(context).kccb, style: TextStyle( @@ -117,12 +130,10 @@ class LoginScreenState extends State ), ), const SizedBox(height: 48), - TextFormField( controller: _customerNumberController, decoration: InputDecoration( labelText: AppLocalizations.of(context).customerNumber, - // prefixIcon: Icon(Icons.person), border: const OutlineInputBorder(), isDense: true, filled: true, @@ -147,7 +158,6 @@ class LoginScreenState extends State }, ), const SizedBox(height: 24), - // Password TextFormField( controller: _passwordController, obscureText: _obscurePassword, @@ -189,7 +199,6 @@ class LoginScreenState extends State }, ), const SizedBox(height: 24), - //Login Button SizedBox( width: 250, child: ElevatedButton( @@ -216,40 +225,7 @@ class LoginScreenState extends State ), ), ), - const SizedBox(height: 15), - - // Padding( - // padding: const EdgeInsets.symmetric(vertical: 16), - // child: Row( - // children: [ - // const Expanded(child: Divider()), - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 8), - // child: Text(AppLocalizations.of(context).or), - // ), - // //const Expanded(child: Divider()), - // ], - // ), - // ), - const SizedBox(height: 25), - - // Register Button - // SizedBox( - // width: 250, - // child: ElevatedButton( - // //disable until registration is implemented - // onPressed: null, - // style: OutlinedButton.styleFrom( - // shape: const StadiumBorder(), - // padding: const EdgeInsets.symmetric(vertical: 16), - // backgroundColor: Theme.of(context).colorScheme.primary, - // foregroundColor: Theme.of(context).colorScheme.onPrimary, - // ), - // child: Text(AppLocalizations.of(context).register, - // style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),), - // ), - // ), ], ), ), diff --git a/lib/features/auth/screens/tnc_required_screen.dart b/lib/features/auth/screens/tnc_required_screen.dart new file mode 100644 index 0000000..23b665a --- /dev/null +++ b/lib/features/auth/screens/tnc_required_screen.dart @@ -0,0 +1,39 @@ + import 'package:flutter/material.dart'; + + class TncRequiredScreen extends StatelessWidget { // Renamed class + const TncRequiredScreen({Key? key}) : super(key: key); + + static const routeName = '/tnc-required'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Terms and Conditions'), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You must accept the Terms and Conditions to use the application.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + // This will take the user back to the previous screen + Navigator.of(context).pop(); + }, + child: const Text('Go Back'), + ), + ], + ), + ), + ), + ); + } + } \ No newline at end of file diff --git a/lib/widgets/tnc_dialog.dart b/lib/widgets/tnc_dialog.dart new file mode 100644 index 0000000..a4870e2 --- /dev/null +++ b/lib/widgets/tnc_dialog.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:kmobile/features/auth/screens/tnc_required_screen.dart'; + +class TncDialog extends StatefulWidget { + // Add a callback function for when the user proceeds + final Future Function() onProceed; + + const TncDialog({Key? key, required this.onProceed}) : super(key: key); + + @override + _TncDialogState createState() => _TncDialogState(); +} + +class _TncDialogState extends State { + bool _isAgreed = false; + bool _isLoading = false; + + void _handleProceed() async { + if (_isLoading) return; + + setState(() { + _isLoading = true; + }); + + // Call the provided onProceed function, which will trigger the cubit + await widget.onProceed(); + + // The dialog will be dismissed by the navigation that happens in the BlocListener + // so we don't need to pop here. If for some reason it's still visible, + // we can add a mounted check and pop. + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Terms and Conditions'), + content: SingleChildScrollView( + child: _isLoading + ? const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Please read and accept our terms and conditions to continue. ' + 'This is a placeholder for the actual terms and conditions text.'), + const SizedBox(height: 16), + Row( + children: [ + Checkbox( + value: _isAgreed, + onChanged: (bool? value) { + setState(() { + _isAgreed = value ?? false; + }); + }, + ), + const Flexible( + child: Text('I agree to the Terms and Conditions')), + ], + ), + ], + ), + ), + actions: [ + TextButton( + // Disable button while loading + onPressed: _isLoading ? null : () { + // Pop with false to indicate disagreement + Navigator.of(context).pop(false); + }, + child: const Text('Disagree'), + ), + ElevatedButton( + // Disable button if not agreed or while loading + onPressed: _isAgreed && !_isLoading ? _handleProceed : null, + child: const Text('Proceed'), + ), + ], + ); + } +} \ No newline at end of file