From 5c8df8ace3f1694a39d8cc25606651c857859c07 Mon Sep 17 00:00:00 2001 From: asif Date: Mon, 10 Nov 2025 12:39:31 +0530 Subject: [PATCH] TNC Route Fixed --- lib/api/services/auth_service.dart | 2 +- lib/features/auth/controllers/auth_cubit.dart | 92 +++---- lib/features/auth/controllers/auth_state.dart | 9 +- lib/features/auth/models/auth_token.dart | 17 +- lib/features/auth/screens/login_screen.dart | 229 ++++++++++++++++-- lib/features/auth/screens/mpin_screen.dart | 21 +- lib/widgets/tnc_dialog.dart | 15 +- 7 files changed, 301 insertions(+), 84 deletions(-) diff --git a/lib/api/services/auth_service.dart b/lib/api/services/auth_service.dart index 333cd42..5bbe894 100644 --- a/lib/api/services/auth_service.dart +++ b/lib/api/services/auth_service.dart @@ -146,7 +146,7 @@ class AuthService { try { final response = await _dio.post( '/api/auth/tnc', - data: {"flag": true}, + data: {"flag": 'Y'}, ); if (response.statusCode != 200) { throw AuthException('Failed to proceed with T&C'); diff --git a/lib/features/auth/controllers/auth_cubit.dart b/lib/features/auth/controllers/auth_cubit.dart index 1195548..e7eec5d 100644 --- a/lib/features/auth/controllers/auth_cubit.dart +++ b/lib/features/auth/controllers/auth_cubit.dart @@ -42,49 +42,57 @@ class AuthCubit extends Cubit { } } - Future login(String customerNo, String password) async { - emit(AuthLoading()); - try { - final (users, authToken) = await _authRepository.login(customerNo, password); + Future login(String customerNo, String password) async { + emit(AuthLoading()); + try { + 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())); - } - } + if (authToken.tnc == false) { + emit(ShowTncDialog(authToken, users)); + } else { + await _checkMpinAndNavigate(users); + } + } 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 onTncDialogResult( + bool agreed, AuthToken authToken, List users) async { + if (agreed) { + try { + await _authRepository.acceptTnc(); + // The user is NOT fully authenticated yet. Just check for MPIN. + await _checkMpinAndNavigate(users); + } catch (e) { + emit(AuthError('Failed to accept TNC: $e')); + } + } else { + emit(NavigateToTncRequiredScreen()); + } + } + + void mpinSetupCompleted() { + if (state is NavigateToMpinSetupScreen) { + final users = (state as NavigateToMpinSetupScreen).users; + emit(Authenticated(users)); + } else { + // Handle unexpected state if necessary + emit(AuthError("Invalid state during MPIN setup completion.")); + } + } + + + Future _checkMpinAndNavigate(List users) async { + final mpin = await _secureStorage.read('mpin'); + if (mpin == null) { + // No MPIN, tell UI to navigate to MPIN setup, carrying user data + emit(NavigateToMpinSetupScreen(users)); + } else { + // MPIN exists, user is authenticated + emit(Authenticated(users)); + } + } - 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 f9fbec7..768eef9 100644 --- a/lib/features/auth/controllers/auth_state.dart +++ b/lib/features/auth/controllers/auth_state.dart @@ -46,6 +46,13 @@ class ShowTncDialog extends AuthState { // States to trigger specific navigations from the UI class NavigateToTncRequiredScreen extends AuthState {} -class NavigateToMpinSetupScreen extends AuthState {} + class NavigateToMpinSetupScreen extends AuthState { + final List users; + + const NavigateToMpinSetupScreen(this.users); + + @override + List get props => [users]; + } 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 586dc49..d0fcea7 100644 --- a/lib/features/auth/models/auth_token.dart +++ b/lib/features/auth/models/auth_token.dart @@ -61,8 +61,21 @@ class AuthToken extends Equatable { return false; } - // Assuming 'tnc' is directly a boolean in the JWT payload - return payloadMap['tnc'] as bool; + final tncValue = payloadMap['tnc']; + + // Handle different representations of 'true' + if (tncValue is bool) { + return tncValue; + } + if (tncValue is String) { + return tncValue.toLowerCase() == 'true'; + } + if (tncValue is int) { + return tncValue == 1; + } + + // Default to false for any other case + return false; } catch (e) { log('Error decoding tnc from token: $e'); // Default to false if decoding fails or 'tnc' is not found/invalid diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart index 9ab4aa8..92753da 100644 --- a/lib/features/auth/screens/login_screen.dart +++ b/lib/features/auth/screens/login_screen.dart @@ -41,51 +41,236 @@ class LoginScreenState extends State @override Widget build(BuildContext context) { - return Scaffold( + // return Scaffold( + // body: BlocConsumer( + // listener: (context, state) async { + // 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).push( // Use push, NOT pushReplacement + // MaterialPageRoute( + // builder: (_) => MPinScreen( + // mode: MPinMode.set, + // onCompleted: (_) { + // // This clears the entire stack and pushes the dashboard + // Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( + // MaterialPageRoute(builder: (_) => const NavigationScaffold()), + // (route) => false, + // ); + // }, + // ), + // ), + // ); + // } 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( + // builder: (_) => SetPasswordScreen( + // customerNo: _customerNumberController.text.trim(), + // ))); + // } else { + // ScaffoldMessenger.of(context) + // .showSnackBar(SnackBar(content: Text(state.message))); + // } + // } + // }, + // 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( + // key: _formKey, + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Image.asset( + // 'assets/images/logo.png', + // width: 150, + // height: 150, + // errorBuilder: (context, error, stackTrace) { + // return Icon( + // Icons.account_balance, + // size: 100, + // color: Theme.of(context).primaryColor, + // ); + // }, + // ), + // const SizedBox(height: 16), + // Text( + // AppLocalizations.of(context).kccb, + // style: TextStyle( + // fontSize: 32, + // fontWeight: FontWeight.bold, + // color: Theme.of(context).primaryColor, + // ), + // ), + // const SizedBox(height: 48), + // TextFormField( + // controller: _customerNumberController, + // decoration: InputDecoration( + // labelText: AppLocalizations.of(context).customerNumber, + // border: const OutlineInputBorder(), + // isDense: true, + // filled: true, + // fillColor: Theme.of(context).scaffoldBackgroundColor, + // enabledBorder: OutlineInputBorder( + // borderSide: BorderSide( + // color: Theme.of(context).colorScheme.outline), + // ), + // focusedBorder: OutlineInputBorder( + // borderSide: BorderSide( + // color: Theme.of(context).colorScheme.primary, + // width: 2), + // ), + // ), + // keyboardType: TextInputType.number, + // textInputAction: TextInputAction.next, + // validator: (value) { + // if (value == null || value.isEmpty) { + // return AppLocalizations.of(context).pleaseEnterUsername; + // } + // return null; + // }, + // ), + // const SizedBox(height: 24), + // TextFormField( + // controller: _passwordController, + // obscureText: _obscurePassword, + // textInputAction: TextInputAction.done, + // onFieldSubmitted: (_) => _submitForm(), + // decoration: InputDecoration( + // labelText: AppLocalizations.of(context).password, + // border: const OutlineInputBorder(), + // isDense: true, + // filled: true, + // fillColor: Theme.of(context).scaffoldBackgroundColor, + // enabledBorder: OutlineInputBorder( + // borderSide: BorderSide( + // color: Theme.of(context).colorScheme.outline), + // ), + // focusedBorder: OutlineInputBorder( + // borderSide: BorderSide( + // color: Theme.of(context).colorScheme.primary, + // width: 2), + // ), + // suffixIcon: IconButton( + // icon: Icon( + // _obscurePassword + // ? Icons.visibility + // : Icons.visibility_off, + // ), + // onPressed: () { + // setState(() { + // _obscurePassword = !_obscurePassword; + // }); + // }, + // ), + // ), + // validator: (value) { + // if (value == null || value.isEmpty) { + // return AppLocalizations.of(context).pleaseEnterPassword; + // } + // return null; + // }, + // ), + // const SizedBox(height: 24), + // SizedBox( + // width: 250, + // child: ElevatedButton( + // onPressed: state is AuthLoading ? null : _submitForm, + // style: ElevatedButton.styleFrom( + // shape: const StadiumBorder(), + // padding: const EdgeInsets.symmetric(vertical: 16), + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: Theme.of(context).primaryColorDark, + // side: BorderSide( + // color: Theme.of(context).colorScheme.outline, + // width: 1), + // elevation: 0, + // ), + // child: state is AuthLoading + // ? const CircularProgressIndicator() + // : Text( + // AppLocalizations.of(context).login, + // style: TextStyle( + // color: Theme.of(context) + // .colorScheme + // .onPrimaryContainer), + // ), + // ), + // ), + // const SizedBox(height: 25), + // ], + // ), + // ), + // ); + // }, + // ), + // ); + return Scaffold( body: BlocConsumer( - listener: (context, state) async { + listener: (context, state) { 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( + 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. + // Pop the dialog before the cubit action + Navigator.of(dialogContext).pop(); 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( + Navigator.of(context).push( // Use push, NOT pushReplacement MaterialPageRoute( builder: (_) => MPinScreen( mode: MPinMode.set, onCompleted: (_) { - Navigator.of(context, rootNavigator: true).pushReplacement( - MaterialPageRoute(builder: (_) => const NavigationScaffold()), - ); + // Call the cubit to signal MPIN setup is complete + context.read().mpinSetupCompleted(); }, ), ), ); - } else if (state is NavigateToDashboardScreen) { - Navigator.of(context).pushReplacement( + } else if (state is Authenticated) { + // This is the single source of truth for navigating to the dashboard + Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const NavigationScaffold()), + (route) => false, ); } else if (state is AuthError) { if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') { @@ -100,7 +285,7 @@ class LoginScreenState extends State } }, builder: (context, state) { - // The commented out section is removed for clarity, the logic is now above. + // The builder part remains largely the same, focusing on UI display return Padding( padding: const EdgeInsets.all(24.0), child: Form( diff --git a/lib/features/auth/screens/mpin_screen.dart b/lib/features/auth/screens/mpin_screen.dart index cfc52bb..f1a16af 100644 --- a/lib/features/auth/screens/mpin_screen.dart +++ b/lib/features/auth/screens/mpin_screen.dart @@ -185,19 +185,16 @@ class _MPinScreenState extends State with TickerProviderStateMixin { ), ); break; - case MPinMode.confirm: - if (widget.initialPin == pin) { - // 1) persist the pin - await storage.write('mpin', pin); + case MPinMode.confirm: + if (widget.initialPin == pin) { + // 1) persist the pin + await storage.write('mpin', pin); - // 3) now clear the entire navigation stack and go to your main scaffold - if (mounted) { - Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const NavigationScaffold()), - (route) => false, - ); - } - } else { + // 2) Call the onCompleted callback to let the parent handle navigation + if (mounted) { + widget.onCompleted?.call(pin); + } + } else { setState(() { _isError = true; errorText = AppLocalizations.of(context).pinsDoNotMatch; diff --git a/lib/widgets/tnc_dialog.dart b/lib/widgets/tnc_dialog.dart index a4870e2..9a24862 100644 --- a/lib/widgets/tnc_dialog.dart +++ b/lib/widgets/tnc_dialog.dart @@ -75,10 +75,17 @@ class _TncDialogState extends State { actions: [ TextButton( // Disable button while loading - onPressed: _isLoading ? null : () { - // Pop with false to indicate disagreement - Navigator.of(context).pop(false); - }, + onPressed: _isLoading + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'You must agree to the terms and conditions to proceed.'), + behavior: SnackBarBehavior.floating, + ), + ); + }, child: const Text('Disagree'), ), ElevatedButton(