Smal Flow

This commit is contained in:
Surendiran 2026-05-28 09:54:10 +05:30
parent 1288a67256
commit f4e28d2629
18 changed files with 297 additions and 142 deletions

View File

@ -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()

View File

@ -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:

View File

@ -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<CustomAppBar> {
// Expanded(
// // flex: Responsive.isDesktop(context) ? 1 : 9,
// child:
if(Responsive.isDesktop(context))
if (!widget.logoOnly && Responsive.isDesktop(context))
Row(
mainAxisSize: MainAxisSize.min,
children: [

View File

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

View File

@ -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<ChatbotHost> {
@override
Widget build(BuildContext context) {
if (!Environment.chatbotEnabled) {
return widget.child;
}
final state = ref.watch(chatbotControllerProvider);
final controller = ref.read(chatbotControllerProvider.notifier);

View File

@ -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<void> main() async {
@ -97,6 +99,10 @@ class _SplashScreenState extends State<SplashScreen> {
Future<String?> 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<String?> tokenRedirectLogic(
'/mailVerify',
'/pinPage',
'/splash',
'/privacypolicy',
'/termsofuse',
];
final hasToken = await TokenService.hasValidToken();
@ -116,6 +124,10 @@ Future<String?> 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<String?> 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<String?> _mobileReauthRedirectIfNeeded(String location) async {
'/sso-login',
'/pinSettingPage',
'/changePin',
'/privacypolicy',
'/termsofuse',
};
final session = SessionManager();
if (session.isMobileAppSessionUnlocked) return null;
@ -371,6 +385,8 @@ Future<void> startApp() async {
await tokenRedirectLogic(context, state),
);
registerAppGoRouter(router);
runApp(
ProviderScope(
child: MyApp(router: router),

View File

@ -1364,14 +1364,10 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
// ? _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<MyEmailVerify> {
),
),
),
SizedBox(
height: Responsive.isDesktop(context)
? _size.height * 0.2
: _size.height * 0.3,
),
],
),
),

View File

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

View File

@ -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<login> {
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<String, dynamic>? _ssoReturnPayloadCaptured;
bool _isProcessingSsoReturn = false;
bool clickedForgotPassword = false;
bool resetPasswordEnable = false;
bool otpFieldShow = false;
@ -87,12 +91,30 @@ class _loginState extends State<login> {
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<login> {
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<login> {
}
if (!mounted) return;
setState(() => _isLoading = true);
// setState(() => _isLoadingSAML = true);
final base = Uri.parse(Environment.SsoLogin);
final qp = <String, String>{'email': emailInput};
@ -196,6 +222,13 @@ class _loginState extends State<login> {
}
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<login> {
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<login> {
}
} finally {
if (mounted && !skipLoadingReset) {
setState(() => _isLoading = false);
// setState(() => _isLoadingSAML = false);
}
}
}
@ -1375,6 +1409,10 @@ class _loginState extends State<login> {
@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<login> {
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<login> {
),
]
],
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<login> {
fontSize: 9,
),
children: <InlineSpan>[
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<login> {
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,
),
],
),
),

View File

@ -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<String?> 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<void> 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<void> completeLoginFromSsoPayloadString(

View File

@ -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<LoginSamlWebViewScreen> {
}
}
Future<void> _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<LoginSamlWebViewScreen> {
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<LoginSamlWebViewScreen> {
@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),
);
}

View File

@ -270,10 +270,10 @@ class _HomeState extends State<Home> {
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<Home> {
} else {
setState(() {
policyDataIsEmpty = 0;
isLoadingGif = false;
});
logDebug('API request failed with status: ${response['status']}');
}

View File

@ -131,7 +131,7 @@ class _policiesState extends State<policies> {
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);

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -4,22 +4,6 @@
<base href="$FLUTTER_BASE_HREF">
<!-- If the IdP redirects to /sso-login?payload=…, many CDNs return 404 (no SPA
fallback). Send the browser to /login?payload=… so index.html loads. -->
<script>
(function () {
try {
var path = window.location.pathname || '';
var search = window.location.search || '';
if (!search || search.indexOf('payload=') === -1) return;
if (!/\/sso-login\/?$/.test(path)) return;
var origin = window.location.origin;
var prefix = path.replace(/\/sso-login\/?$/, '') || '';
window.location.replace(origin + prefix + '/login' + search);
} catch (e) {}
})();
</script>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Nhance Benefits">