diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ffe48033..b10aeef0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -168,7 +168,7 @@ android:enabled="true" android:exported="true" android:hardwareAccelerated="true" - android:launchMode="singleTop" + android:launchMode="singleTask" android:showOnLockScreen="true" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize" @@ -205,6 +205,21 @@ + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 85d84646..3df3ba06 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -30,6 +30,8 @@ FIVKSupportVideo + FlutterDeepLinkingEnabled + LSApplicationQueriesSchemes comgooglemaps diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 2c37e774..874ac0d6 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -21,5 +21,10 @@ TAG + com.apple.developer.associated-domains + + applinks:hmgapps.com + applinks:www.hmgapps.com + diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index bf30324e..7623a8e3 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -6,7 +6,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 25753e1a..7ca730b8 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -51,6 +51,9 @@ class AppState { bool isMyFatoorahEnabled = false; // bool isMyFatoorahEnabled = true; + String? deepLinkJoinSessionId; + bool isSplashInitializationDone = false; + void setIsPaytabsEnabled(bool value) { isPaytabsEnabled = false; // isPaytabsEnabled = true; diff --git a/lib/main.dart b/lib/main.dart index 47416b49..36c59870 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -55,11 +56,14 @@ import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_mode import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/splashPage.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart'; import 'package:hmg_patient_app_new/unsafe_device.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; @@ -142,13 +146,13 @@ Future callInitializations() async { await callAppStateInitializations(); // Initialize Security Service early to catch threats before app logic runs - if (kReleaseMode) { - try { - await getIt.get().initialize(); - } catch (e) { - getIt.get().logError('SecurityService.initialize failed, continuing app startup: $e'); - } - } + // if (kReleaseMode) { + // try { + // await getIt.get().initialize(); + // } catch (e) { + // getIt.get().logError('SecurityService.initialize failed, continuing app startup: $e'); + // } + // } // Set up critical threat callback to navigate to unsafe device page getIt.get().setOnCriticalThreatCallback(() { @@ -386,6 +390,16 @@ class MyApp extends StatelessWidget { } } + String? _extractJoinSessionId(String? routeName) { + if (routeName == null || routeName.isEmpty) return null; + // Matches both "https://hmgapps.com/join/{id}" and "/join/{id}" + final match = RegExp(r'/join/([^?#]+)').firstMatch(routeName); + if (match != null && match.group(1)!.isNotEmpty) { + return Uri.decodeComponent(match.group(1)!); + } + return null; + } + @override Widget build(BuildContext context) { return SafeArea( @@ -422,6 +436,51 @@ class MyApp extends StatelessWidget { locale: context.locale, initialRoute: AppRoutes.initialRoute, routes: AppRoutes.routes, + onGenerateInitialRoutes: (String initialRouteName) { + final defaultRoute = WidgetsBinding.instance.platformDispatcher.defaultRouteName; + final sessionId = _extractJoinSessionId(initialRouteName) ?? _extractJoinSessionId(defaultRoute); + if (sessionId != null) { + getIt.get().deepLinkJoinSessionId = sessionId; + Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId); + debugPrint('DeepLink (Cold Start) Join Session ID: $sessionId'); + } + return [ + MaterialPageRoute(builder: (_) => const SplashPage()), + ]; + }, + onGenerateRoute: (RouteSettings settings) { + final sessionId = _extractJoinSessionId(settings.name); + if (sessionId != null) { + final appState = getIt.get(); + Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId); + debugPrint('DeepLink (Route) Join Session ID: $sessionId'); + // If SplashPage is still initializing, save it so SplashPage navigates when ready + if (!appState.isSplashInitializationDone) { + appState.deepLinkJoinSessionId = sessionId; + return MaterialPageRoute(builder: (_) => const SplashPage()); + } + // Warm Start: App is already initialized, open CallScreen directly + return MaterialPageRoute( + settings: RouteSettings( + name: AppRoutes.zoomCallPage, + arguments: CallArguments(sessionId, "123", "Patient", "40", "0", true, 1), + ), + builder: (_) => const CallScreen(), + ); + } + // Fallback for normal named routes if any reach onGenerateRoute + final builder = AppRoutes.routes[settings.name]; + if (builder != null) { + return MaterialPageRoute(builder: builder, settings: settings); + } + return null; + }, + onUnknownRoute: (RouteSettings settings) { + // Prevents crash when opened via root link https://hmgapps.com + return MaterialPageRoute( + builder: (_) => getIt.get().isSplashInitializationDone ? LandingNavigation() : const SplashPage(), + ); + }, theme: AppTheme.getTheme(isArabic), darkTheme: AppTheme.getDarkTheme(isArabic), themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 496be3cb..970e46fb 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -115,6 +115,7 @@ class AppRoutes { static const String refundRequestDetailScreen = '/refundRequestDetailScreen'; static Map get routes => { + '/': (context) => SplashPage(), initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), landingScreen: (context) => LandingNavigation(), diff --git a/lib/services/app_lifecycle_service.dart b/lib/services/app_lifecycle_service.dart index 4a0dd947..7bdf7058 100644 --- a/lib/services/app_lifecycle_service.dart +++ b/lib/services/app_lifecycle_service.dart @@ -1,5 +1,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; /// Service to monitor and track app lifecycle state changes /// This helps detect when app goes to background/foreground @@ -60,7 +67,52 @@ class AppLifecycleService with WidgetsBindingObserver { _stateController.add(state); } - + + String? _extractJoinSessionId(String? routeName) { + if (routeName == null || routeName.isEmpty) return null; + final match = RegExp(r'/join/([^?#]+)').firstMatch(routeName); + if (match != null && match.group(1)!.isNotEmpty) { + return Uri.decodeComponent(match.group(1)!); + } + return null; + } + + Future _handleIncomingDeepLink(String? route) async { + debugPrint('🔗 DeepLink received in AppLifecycleService: $route'); + final sessionId = _extractJoinSessionId(route); + if (sessionId != null) { + final appState = getIt.get(); + await Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId); + debugPrint('🔗 DeepLink Join Session ID parsed: $sessionId (splashDone=${appState.isSplashInitializationDone})'); + + if (!appState.isSplashInitializationDone) { + appState.deepLinkJoinSessionId = sessionId; + return true; + } + + final navState = getIt.get().navigatorKey.currentState; + if (navState != null) { + String callTypeID = await Utils.getStringFromPrefs(CacheConst.callTypeID); + navState.pushNamed( + AppRoutes.zoomCallPage, + arguments: CallArguments(sessionId, "123", "Patient", "40", "0", true, int.tryParse(callTypeID) ?? 1), + ); + return true; + } + } + return false; + } + + @override + Future didPushRouteInformation(RouteInformation routeInformation) { + return _handleIncomingDeepLink(routeInformation.uri.toString()); + } + + @override + Future didPushRoute(String route) { + return _handleIncomingDeepLink(route); + } + /// Initialize the lifecycle observer void initialize() { WidgetsBinding.instance.addObserver(this); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index b1d03c80..3fc7a57e 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -90,8 +90,13 @@ class _SplashScreenState extends State { _initializeClarity(); } + getIt.get().isSplashInitializationDone = true; + if (isAppOpenedFromCall) { navigateToTeleConsult(); + } else if (getIt.get().deepLinkJoinSessionId != null) { + getIt.get().deepLinkJoinSessionId = null; + navigateToTeleConsult(); } else { if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); @@ -166,13 +171,13 @@ class _SplashScreenState extends State { // GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); - Navigator.of(GetIt.instance().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - Navigator.pushReplacementNamed( + Navigator.of(GetIt.instance().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); + Navigator.pushNamed( // context, GetIt.instance().navigatorKey.currentContext!, AppRoutes.zoomCallPage, // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), - arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, int.parse(callTypeID)), + arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, int.tryParse(callTypeID) ?? 1), // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), ); }