diff --git a/android/app/build.gradle b/android/app/build.gradle index bb4c586..15dbe91 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -19,12 +19,12 @@ if (project.hasProperty('google-services.json')) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '61' + flutterVersionCode = '63' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '2.0.23' + flutterVersionName = '2.0.25' } def keystoreProperties = new Properties() diff --git a/lib/config/environment.dart b/lib/config/environment.dart index 227de78..151210b 100755 --- a/lib/config/environment.dart +++ b/lib/config/environment.dart @@ -4,6 +4,9 @@ enum Flavor { dev, uat, prod, prod1 } class Environment { static Flavor flavor = Flavor.dev; // overwritten by each main_*.dart + /// Chatbot FAB + window. Set to `true` when ready to enable. + static const bool chatbotEnabled = false; + static bool get isProd => flavor == Flavor.prod || flavor == Flavor.prod1; static String get apiUrl { @@ -51,12 +54,11 @@ class Environment { case Flavor.uat: return "https://appstage.nhanceindia.in/apex/saml/login"; case Flavor.prod: - return "https://app.nhanceindia.in/saml/login"; + return "https://app.nhanceindia.in/enrolment/saml/login"; case Flavor.prod1: - return "https://app.nhanceindia.in/saml/login"; + return "https://app.nhanceindia.in/enrolment/saml/login"; } } - static String get baseHref { switch (flavor) { case Flavor.dev: diff --git a/lib/customAppBar/customAppBar.dart b/lib/customAppBar/customAppBar.dart index 98a0094..535dfeb 100755 --- a/lib/customAppBar/customAppBar.dart +++ b/lib/customAppBar/customAppBar.dart @@ -11,6 +11,11 @@ import '../pages/service/SessionManager.dart'; import '../pages/service/TokenService.dart'; import '../pages/service/popup_helper.dart'; class CustomAppBar extends StatefulWidget implements PreferredSizeWidget { + /// When true, shows only the client logo (no nav menu). + final bool logoOnly; + + const CustomAppBar({super.key, this.logoOnly = false}); + @override _CustomAppBarState createState() => _CustomAppBarState(); @@ -102,7 +107,7 @@ class _CustomAppBarState extends State { // Expanded( // // flex: Responsive.isDesktop(context) ? 1 : 9, // child: - if(Responsive.isDesktop(context)) + if (!widget.logoOnly && Responsive.isDesktop(context)) Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/customAppBar/nhance_loading_screen.dart b/lib/customAppBar/nhance_loading_screen.dart new file mode 100644 index 0000000..84444d5 --- /dev/null +++ b/lib/customAppBar/nhance_loading_screen.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +/// Full-screen Nhance loader (same style as splash / home overlays). +class NhanceLoadingScreen extends StatelessWidget { + const NhanceLoadingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + color: const Color(0x98FFFCE5), + child: Center( + child: Image.asset( + 'assets/nhance-loader.gif', + height: 60, + width: 60, + ), + ), + ), + ); + } +} diff --git a/lib/features/chatbot/presentation/chatbot_host.dart b/lib/features/chatbot/presentation/chatbot_host.dart index da64843..9c3fcdd 100644 --- a/lib/features/chatbot/presentation/chatbot_host.dart +++ b/lib/features/chatbot/presentation/chatbot_host.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:nhance_app_pwa/config/environment.dart'; import 'package:nhance_app_pwa/customAppBar/responsive.dart'; import 'package:nhance_app_pwa/features/chatbot/application/chatbot_controller.dart'; import 'package:nhance_app_pwa/features/chatbot/presentation/widgets/chatbot_fab.dart'; @@ -36,6 +37,10 @@ class _ChatbotHostState extends ConsumerState { @override Widget build(BuildContext context) { + if (!Environment.chatbotEnabled) { + return widget.child; + } + final state = ref.watch(chatbotControllerProvider); final controller = ref.read(chatbotControllerProvider.notifier); diff --git a/lib/main.dart b/lib/main.dart index a0b1a8c..21b63a6 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:nhance_app_pwa/pages/enrollment/addons.dart'; import 'package:nhance_app_pwa/pages/enrollment/empDetails.dart'; import 'package:nhance_app_pwa/pages/enrollment/empReview.dart'; import 'package:nhance_app_pwa/pages/login.dart'; +import 'package:nhance_app_pwa/pages/login_saml_auth.dart'; import 'package:nhance_app_pwa/pages/sso_login_return_page.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/AddPolicyScreen.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/claimprocess.dart'; @@ -47,6 +48,7 @@ import 'package:go_router/go_router.dart'; import 'config/environment.dart'; import 'logger.dart'; +import 'router/app_router.dart'; // import 'dart:html' as html; Future main() async { @@ -97,6 +99,10 @@ class _SplashScreenState extends State { Future tokenRedirectLogic( BuildContext context, GoRouterState state) async { logDebug('ABCDEFGH'); + + final ssoLoginRedirect = redirectLoginWithSsoPayload(state); + if (ssoLoginRedirect != null) return ssoLoginRedirect; + const guestRoutes = [ '/login', '/sso-login', @@ -104,6 +110,8 @@ Future tokenRedirectLogic( '/mailVerify', '/pinPage', '/splash', + '/privacypolicy', + '/termsofuse', ]; final hasToken = await TokenService.hasValidToken(); @@ -116,6 +124,10 @@ Future tokenRedirectLogic( if (hasToken) { await session.restoreSession(); + final destination = await TokenService.defaultRouteForStoredTokens(); + if (location == '/home' && destination == '/empDetails') { + return '/empDetails'; + } } if (!kIsWeb && hasToken) { @@ -123,12 +135,12 @@ Future tokenRedirectLogic( if (reauth != null) return reauth; } - // Logged in on a guest route → home (only when mobile session is unlocked). + // Logged in on a guest route → home or enrollment (pre-only). if (hasToken && guestRoutes.contains(location)) { if (!kIsWeb && !session.isMobileAppSessionUnlocked) { return null; } - return '/home'; + return await TokenService.defaultRouteForStoredTokens(); } if (!hasToken && guestRoutes.contains(location)) { @@ -157,6 +169,8 @@ Future _mobileReauthRedirectIfNeeded(String location) async { '/sso-login', '/pinSettingPage', '/changePin', + '/privacypolicy', + '/termsofuse', }; final session = SessionManager(); if (session.isMobileAppSessionUnlocked) return null; @@ -371,6 +385,8 @@ Future startApp() async { await tokenRedirectLogic(context, state), ); + registerAppGoRouter(router); + runApp( ProviderScope( child: MyApp(router: router), diff --git a/lib/pages/email_verify.dart b/lib/pages/email_verify.dart index 524c775..f044e70 100755 --- a/lib/pages/email_verify.dart +++ b/lib/pages/email_verify.dart @@ -1364,14 +1364,10 @@ class _MyEmailVerifyState extends State { // ? _size.height * 0.1 // : _size.height * 0.2, // ), + SizedBox( - height: Responsive.isDesktop(context) - ? _size.height * 0.2 - : _size.height * 0.3, + height: 15, ), - // SizedBox( - // height: _size.height * 0.1, - // ), Container( alignment: Responsive.isDesktop(context) ? Alignment.center : Alignment.bottomCenter, padding: @@ -1411,6 +1407,11 @@ class _MyEmailVerifyState extends State { ), ), ), + SizedBox( + height: Responsive.isDesktop(context) + ? _size.height * 0.2 + : _size.height * 0.3, + ), ], ), ), diff --git a/lib/pages/enrollment/service/api_service.dart b/lib/pages/enrollment/service/api_service.dart index 06b0694..11d3eed 100755 --- a/lib/pages/enrollment/service/api_service.dart +++ b/lib/pages/enrollment/service/api_service.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import '../../../config/environment.dart'; @@ -526,10 +525,7 @@ class ApiService { } Future _clearLocalStorageAndRedirect() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.clear(); - // Assuming you have access to the context - ToastHelper.showErrorToast(context, 'Session Out'); - Navigator.pushNamed(context, 'phone'); + if (!context.mounted) return; + await TokenService().logout(context); } } diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 32e35f9..54753d0 100755 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,6 +1,7 @@ // import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; @@ -14,6 +15,7 @@ import 'dart:io'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:google_fonts/google_fonts.dart'; import '../config/environment.dart'; +import '../customAppBar/nhance_loading_screen.dart'; import '../customAppBar/responsive.dart'; import '../customAppBar/toastHelper.dart'; import '../models/platform_helper_mobile.dart' @@ -56,12 +58,14 @@ class _loginState extends State { late String _verificationId; int? _resendToken; bool _isLoading = false; + bool _isLoadingSAML = false; bool isEmailFieldVisible = false; bool _obscurePassword = true; bool _resetObscurePassword = true; bool _obscureConfirmPassword = true; late SessionManager session; Map? _ssoReturnPayloadCaptured; + bool _isProcessingSsoReturn = false; bool clickedForgotPassword = false; bool resetPasswordEnable = false; bool otpFieldShow = false; @@ -87,12 +91,30 @@ class _loginState extends State { bool get isPasswordValid => hasMinLength && hasUpperLower && hasNumber && hasSpecialChar; + final TapGestureRecognizer _privacyPolicyTap = TapGestureRecognizer(); + final TapGestureRecognizer _termsOfUseTap = TapGestureRecognizer(); + + static final TextStyle _legalLinkStyle = GoogleFonts.poppins( + color: const Color(0xFF00989E), + fontSize: 9, + decoration: TextDecoration.underline, + ); + @override void initState() { - countryController.text = "+91"; super.initState(); + countryController.text = "+91"; + _privacyPolicyTap.onTap = () { + if (mounted) context.go('/privacypolicy'); + }; + _termsOfUseTap.onTap = () { + if (mounted) context.go('/termsofuse'); + }; if (kIsWeb) { _captureSsoPayloadFromBrowserUrl(); + if (_ssoReturnPayloadCaptured != null) { + _isProcessingSsoReturn = true; + } WidgetsBinding.instance .addPostFrameCallback((_) => _tryConsumeSsoPayloadFromReturnUrl()); } @@ -138,15 +160,19 @@ class _loginState extends State { if (decoded == null || !mounted) return; + if (mounted) setState(() => _isProcessingSsoReturn = true); try { await completeLoginFromSsoPayloadMap(context, decoded); } finally { if (kIsWeb) stripPayloadParamFromBrowserUrl(); + if (mounted) setState(() => _isProcessingSsoReturn = false); } } @override void dispose() { + _privacyPolicyTap.dispose(); + _termsOfUseTap.dispose(); super.dispose(); } @@ -184,7 +210,7 @@ class _loginState extends State { } if (!mounted) return; - setState(() => _isLoading = true); + // setState(() => _isLoadingSAML = true); final base = Uri.parse(Environment.SsoLogin); final qp = {'email': emailInput}; @@ -196,6 +222,13 @@ class _loginState extends State { } final samlEntry = base.replace(queryParameters: qp); + final samlError = await fetchSamlEntryFailureMessage(samlEntry); + if (!mounted) return; + if (samlError != null) { + ToastHelper.showErrorToast(context, samlError); + return; + } + if (kIsWeb) { skipLoadingReset = true; ssoRedirectSameTab(samlEntry.toString()); @@ -211,8 +244,9 @@ class _loginState extends State { if (!mounted) return; if (result != null && result.isNotEmpty) { + if (mounted) setState(() => _isProcessingSsoReturn = true); await completeLoginFromSsoPayloadMap(context, result); - } + } } catch (e) { if (mounted) { ToastHelper.showErrorToast( @@ -221,7 +255,7 @@ class _loginState extends State { } } finally { if (mounted && !skipLoadingReset) { - setState(() => _isLoading = false); + // setState(() => _isLoadingSAML = false); } } } @@ -1375,6 +1409,10 @@ class _loginState extends State { @override Widget build(BuildContext context) { + if (_isProcessingSsoReturn) { + return const NhanceLoadingScreen(); + } + Size _size = MediaQuery.of(context).size; EdgeInsets marginInsets = EdgeInsets.zero; @@ -1779,7 +1817,7 @@ class _loginState extends State { width: double.infinity, height: 45, child: ElevatedButton( - onPressed: _isLoading ? null : _handleSsoLoginButtonTap, + onPressed: _handleSsoLoginButtonTap, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.black, @@ -2490,17 +2528,10 @@ class _loginState extends State { ), ] ], + SizedBox( - height: _size.width <= 1100 ? 0 : 0, + height: 10, ), - SizedBox( - height: Responsive.isDesktop(context) - ? _size.height * 0.1 - : _size.height * 0.2, - ), - // SizedBox( - // height: _size.height * 0.1, - // ), Container( alignment: Alignment.bottomCenter, padding: @@ -2515,32 +2546,13 @@ class _loginState extends State { fontSize: 9, ), children: [ - WidgetSpan( - child: MouseRegion( - cursor: SystemMouseCursors - .click, - child: GestureDetector( - onTap: () { - context.go( - '/privacypolicy'); - // Navigator.pushNamed( - // context, - // 'privacypolicy'); - }, - child: Text( - 'privacy policy ', - style: - GoogleFonts.poppins( - color: - Color(0xFF00989E), - fontSize: 9, - decoration: - TextDecoration - .underline, - ), - ), - ), - ), + TextSpan( + text: 'privacy policy ', + style: _legalLinkStyle, + recognizer: + _privacyPolicyTap, + mouseCursor: + SystemMouseCursors.click, ), TextSpan( text: 'and ', @@ -2549,37 +2561,25 @@ class _loginState extends State { fontSize: 9, ), ), - WidgetSpan( - child: MouseRegion( - cursor: SystemMouseCursors - .click, - child: GestureDetector( - onTap: () { - context - .go('/termsofuse'); - // Navigator.pushNamed( - // context, - // 'termsofuse'); - }, - child: Text( - 'terms of use', - style: - GoogleFonts.poppins( - color: - Color(0xFF00989E), - fontSize: 9, - decoration: - TextDecoration - .underline, - ), - ), - ), - ), + TextSpan( + text: 'terms of use', + style: _legalLinkStyle, + recognizer: _termsOfUseTap, + mouseCursor: + SystemMouseCursors.click, ), ], ), ), ), + SizedBox( + height: _size.width <= 1100 ? 0 : 0, + ), + SizedBox( + height: Responsive.isDesktop(context) + ? _size.height * 0.1 + : _size.height * 0.2, + ), ], ), ), diff --git a/lib/pages/login_saml_auth.dart b/lib/pages/login_saml_auth.dart index c11757a..644b71d 100644 --- a/lib/pages/login_saml_auth.dart +++ b/lib/pages/login_saml_auth.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'helpers/browser_href.dart'; import 'package:http/http.dart' as http; import 'package:jwt_decode/jwt_decode.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -15,24 +16,38 @@ import 'service/TokenService.dart'; /// Where the IdP should return the browser after SAML (web). /// -/// Uses **`/login`** (not `/sso-login`) so static hosts that do not rewrite -/// unknown paths to `index.html` still return **200** and Flutter can read -/// `?payload=`. `/sso-login` is still supported via `web/index.html` redirect. +/// Uses **`/sso-login`** so the app shows a loading screen (not the login form) +/// and then navigates straight to home or enrollment after tokens are saved. String buildWebSsoRelayStateUrl() { if (!kIsWeb) return ''; final origin = Uri.base.origin; final frag = Uri.base.fragment; - // Hash routing: e.g. `http://host/#/login` → return with payload on `#/login` if (frag.startsWith('/')) { - return '$origin/#/login'; + return '$origin/#/sso-login'; + } + var path = Uri.base.path; + if (path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + if (path.endsWith('/sso-login')) { + path = path.substring(0, path.length - '/sso-login'.length); } final href = Environment.baseHref; - if (href.isEmpty || href == '/') { - return '$origin/login'; + final basePath = (href.isEmpty || href == '/') + ? (path.isEmpty ? '' : path) + : (href.endsWith('/') ? href.substring(0, href.length - 1) : href); + if (basePath.isEmpty) { + return '$origin/sso-login'; } - final trimmed = - href.endsWith('/') ? href.substring(0, href.length - 1) : href; - return '$origin$trimmed/login'; + return '$origin$basePath/sso-login'; +} + +/// When SAML returns to `/login?payload=`, send users to [SsoLoginReturnPage]. +String? redirectLoginWithSsoPayload(GoRouterState state) { + if (state.matchedLocation != '/login') return null; + final payload = readPayloadFromRouterUri(state.uri); + if (payload == null || payload.isEmpty) return null; + return '/sso-login?payload=${Uri.encodeQueryComponent(payload)}'; } /// Decodes the `payload` query value (base64 / base64url) into a JSON map. @@ -153,6 +168,52 @@ SsoPayloadJwtDecode decodeJwtClaimsFromSsoPayloadMap( ); } +/// Parses SAML entry API JSON such as +/// `{"status":"failed","code":404,"message":"SAML not configured for this domain"}`. +String? parseSamlApiFailureMessage(String body) { + if (body.trim().isEmpty) return null; + try { + final decoded = json.decode(body); + if (decoded is Map && decoded['status']?.toString() == 'failed') { + final msg = decoded['message']?.toString(); + if (msg != null && msg.isNotEmpty) return msg; + return 'SSO sign-in was not successful'; + } + } catch (_) {} + return null; +} + +/// GETs the SAML entry URL without following redirects. Returns an error +/// message when the server responds with a failed JSON body (e.g. unknown domain). +Future fetchSamlEntryFailureMessage(Uri samlEntry) async { + final client = http.Client(); + try { + final request = http.Request('GET', samlEntry)..followRedirects = false; + final streamed = await client + .send(request) + .timeout(const Duration(seconds: 30)); + final response = await http.Response.fromStream(streamed); + + final jsonError = parseSamlApiFailureMessage(response.body); + if (jsonError != null) return jsonError; + + if (response.statusCode >= 300 && response.statusCode < 400) { + return null; + } + if (response.statusCode == 200) return null; + + if (response.statusCode >= 400) { + return 'Unable to start SSO sign-in'; + } + return null; + } catch (e) { + logDebug('SAML entry preflight failed: $e'); + return null; + } finally { + client.close(); + } +} + String prettySsoJsonForConsole(Object? value) { if (value == null) return 'null'; try { @@ -257,14 +318,14 @@ Future completeLoginFromSsoPayloadMap( if (!context.mounted) return; - if (postToken != null && postToken.isNotEmpty) { + if (kIsWeb) stripPayloadParamFromBrowserUrl(); + + final destination = await TokenService.defaultRouteForStoredTokens(); + SessionManager().unlockMobileAppSession(); + if (destination == '/home') { ToastHelper.showSuccessToast(context, 'Successfully Logged In'); - SessionManager().unlockMobileAppSession(); - context.go('/home'); - } else { - SessionManager().unlockMobileAppSession(); - context.go('/empDetails'); } + if (context.mounted) context.replace(destination); } Future completeLoginFromSsoPayloadString( diff --git a/lib/pages/login_saml_webview_screen.dart b/lib/pages/login_saml_webview_screen.dart index ef33ccf..75367e2 100644 --- a/lib/pages/login_saml_webview_screen.dart +++ b/lib/pages/login_saml_webview_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import '../customAppBar/customAppBar.dart'; +import '../customAppBar/toastHelper.dart'; import 'login_saml_auth.dart'; /// Mobile: in-app WebView for SAML entry; when URL contains `payload=`, decodes and pops. @@ -27,6 +29,21 @@ class _LoginSamlWebViewScreenState extends State { } } + Future _tryFinishFromPageBody() async { + if (_finished || !mounted) return; + try { + final raw = await _controller.runJavaScriptReturningResult( + 'document.body ? document.body.innerText : ""', + ); + final text = raw.toString().trim(); + final msg = parseSamlApiFailureMessage(text); + if (msg == null || !mounted) return; + _finished = true; + ToastHelper.showErrorToast(context, msg); + Navigator.of(context).pop(); + } catch (_) {} + } + void _tryFinishFromHttpError(HttpResponseError error) { if (_finished || !mounted) return; final status = error.response?.statusCode; @@ -57,7 +74,10 @@ class _LoginSamlWebViewScreenState extends State { return NavigationDecision.navigate; }, onPageStarted: (String url) => _tryFinishWithUrl(url), - onPageFinished: (String url) => _tryFinishWithUrl(url), + onPageFinished: (String url) async { + _tryFinishWithUrl(url); + if (!_finished) await _tryFinishFromPageBody(); + }, onUrlChange: (UrlChange change) { final url = change.url; if (url != null && url.isNotEmpty) { @@ -79,7 +99,8 @@ class _LoginSamlWebViewScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Sign in with SSO')), + backgroundColor: Colors.white, + appBar: const CustomAppBar(logoOnly: true), body: WebViewWidget(controller: _controller), ); } diff --git a/lib/pages/postEnrollment/home.dart b/lib/pages/postEnrollment/home.dart index 42c93b2..24919a2 100755 --- a/lib/pages/postEnrollment/home.dart +++ b/lib/pages/postEnrollment/home.dart @@ -270,10 +270,10 @@ class _HomeState extends State { final response = await apiService.getActiveAndInactivePolicyDetails( client_id, empCodeString, status, client_branch_id, mobileNo,emailId); logDebug('check 1'); + setState(() { + isLoadingGif = false; + }); if (response['status'] == 'success') { - setState(() { - isLoadingGif = false; - }); emp_name = response['emp_name']; policyList = List.from(response['data'] ?? []); retailPolicyDetails = List.from(response['retail_policy_data'] ?? []); @@ -307,7 +307,6 @@ class _HomeState extends State { } else { setState(() { policyDataIsEmpty = 0; - isLoadingGif = false; }); logDebug('API request failed with status: ${response['status']}'); } diff --git a/lib/pages/postEnrollment/policies.dart b/lib/pages/postEnrollment/policies.dart index db760c6..2b49606 100755 --- a/lib/pages/postEnrollment/policies.dart +++ b/lib/pages/postEnrollment/policies.dart @@ -131,7 +131,7 @@ class _policiesState extends State { String convertDateFormat(String date) { // Define input and output date formats DateFormat inputFormat = DateFormat('dd-MMM-yyyy'); - DateFormat outputFormat = DateFormat('d MMM yyyy'); + DateFormat outputFormat = DateFormat('d MMM yyyy'); // Parse the input date string DateTime parsedDate = inputFormat.parse(date); diff --git a/lib/pages/service/TokenService.dart b/lib/pages/service/TokenService.dart index 8754b19..85af6af 100755 --- a/lib/pages/service/TokenService.dart +++ b/lib/pages/service/TokenService.dart @@ -10,7 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../config/environment.dart'; import '../../customAppBar/toastHelper.dart'; import '../../logger.dart'; -import '../../models/platform_helper_mobile.dart'; +import '../../router/app_router.dart'; import 'SessionManager.dart'; import 'web_session_storage.dart'; @@ -70,6 +70,15 @@ class TokenService { (pre != null && pre.isNotEmpty); } + /// Post token → home; pre-only → enrollment ([empDetails]). + static Future defaultRouteForStoredTokens() async { + final post = await getPostToken(); + if (post != null && post.isNotEmpty) return '/home'; + final pre = await getPreToken(); + if (pre != null && pre.isNotEmpty) return '/empDetails'; + return '/login'; + } + /// Clears auth tokens that older web builds stored in SharedPreferences (localStorage). /// Call once at startup so they cannot be mistaken for a valid session. static Future clearLegacyWebAuthFromSharedPreferences() async { @@ -131,17 +140,35 @@ class TokenService { return null; } + void _goAfterLogout(BuildContext context, String location) { + if (context.mounted && GoRouter.maybeOf(context) != null) { + context.go(location); + } else { + navigateAppTo(location); + } + } + Future logout(BuildContext context) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); String? mobileNumber = prefs.getString('empMobileNo'); String? mailID = prefs.getString('empEmailid'); - final isMpinSkipped = await checkLoginPin(); - logDebug('isMpinSkipped $isMpinSkipped'); + + final postToken = await getPostToken(); + final hasPostToken = postToken != null && postToken.isNotEmpty; + + String? isMpinSkipped; + if (!kIsWeb && hasPostToken) { + isMpinSkipped = await checkLoginPin(); + } + logDebug('isMpinSkipped $isMpinSkipped hasPostToken $hasPostToken'); await SessionManager().clear(); - if (isMobilePlatform()) { + if (kIsWeb) { + await prefs.clear(); + _goAfterLogout(context, '/login'); + } else { if (mobileNumber != null) { await prefs.setString('empMobileNo', mobileNumber); } @@ -151,15 +178,11 @@ class TokenService { if (isMpinSkipped != null) { await prefs.setString('is_mpin_skipped', isMpinSkipped); } - if (isMpinSkipped != null && isMpinSkipped == '0') { - context.go('/pinPage'); + if (hasPostToken && isMpinSkipped != null && isMpinSkipped == '0') { + _goAfterLogout(context, '/pinPage'); } else { - context.go('/login'); + _goAfterLogout(context, '/login'); } - } else { - await prefs.clear(); - ToastHelper.showSuccessToast(context, 'logout'); - context.go('/login'); } } } diff --git a/lib/pages/sso_login_return_page.dart b/lib/pages/sso_login_return_page.dart index b7152e6..e869d8c 100644 --- a/lib/pages/sso_login_return_page.dart +++ b/lib/pages/sso_login_return_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../customAppBar/nhance_loading_screen.dart'; import '../customAppBar/toastHelper.dart'; import '../logger.dart'; import 'helpers/browser_href.dart'; @@ -50,13 +51,15 @@ class _SsoLoginReturnPageState extends State { } logDebug('SSO return: decoded payload (keys)=${decoded.keys.toList()}'); - await completeLoginFromSsoPayloadMap(context, decoded); + try { + await completeLoginFromSsoPayloadMap(context, decoded); + } finally { + if (kIsWeb) stripPayloadParamFromBrowserUrl(); + } } @override Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); + return const NhanceLoadingScreen(); } } diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart new file mode 100644 index 0000000..e39c0b1 --- /dev/null +++ b/lib/router/app_router.dart @@ -0,0 +1,17 @@ +import 'package:go_router/go_router.dart'; + +/// Set once in [startApp] so services (e.g. [TokenService.logout]) can navigate +/// when [BuildContext] has no [GoRouter] ancestor (e.g. ApiService from MyApp). +GoRouter? appGoRouter; + +void registerAppGoRouter(GoRouter router) { + appGoRouter = router; +} + +void navigateAppTo(String location) { + final router = appGoRouter; + if (router == null) { + throw StateError('GoRouter not registered. Call registerAppGoRouter first.'); + } + router.go(location); +} diff --git a/pubspec.yaml b/pubspec.yaml index cd3a162..830a0c3 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,8 +17,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. #version: 1.2.41+98 -version: 1.0.35+41 -#version: 2.0.23+61 +version: 1.0.37+43 +#version: 2.0.25+63 environment: sdk: '>=3.3.3 <4.0.0' diff --git a/web/index.html b/web/index.html index 3d3f5a1..8308ee4 100755 --- a/web/index.html +++ b/web/index.html @@ -4,22 +4,6 @@ - - -