From 902b357fbd16ed05a375cd567c6bf0504408f004 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Thu, 9 Jul 2026 15:03:20 +0530 Subject: [PATCH 1/7] refactor(CHIM-620): migrate react navigation from browserrouter to hashrouter --- src/App.tsx | 7 +- src/ProtectedRoute.tsx | 7 +- src/analytics/clickUtil.ts | 9 +- src/analytics/profileClickUtil.ts | 7 +- src/common/backButtonRegistry.ts | 3 +- src/components/coloring/ColorPalette.tsx | 3 +- src/components/coloring/ColoringBoard.tsx | 19 ++- src/components/coloring/useSvgColoring.ts | 3 +- .../leaderboard/LeaderboardRewards.tsx | 12 +- .../StickerBookPreviewModal.logic.ts | 5 +- src/components/parent/DeleteParentAccount.tsx | 3 +- src/components/parent/ParentLogout.tsx | 3 +- .../profileDetails/ProfileDetails.tsx | 14 +- .../stickerBook/StickerBookBoard.tsx | 7 +- .../SchoolDetailsComponents/SchoolNotes.tsx | 3 +- src/ops-console/components/Sidebar.tsx | 3 +- src/pages/AppLangSelection.tsx | 3 +- src/pages/DisplayStudents.tsx | 3 +- src/pages/Home.tsx | 3 +- src/pages/HotUpdate.tsx | 3 +- src/pages/Leaderboard.tsx | 11 +- src/pages/LidoPlayer.tsx | 9 +- src/pages/LiveQuizGame.tsx | 5 +- src/pages/LiveQuizLeaderBoard.tsx | 3 +- src/pages/LiveQuizRoom.tsx | 3 +- src/pages/LiveQuizRoomResult.tsx | 3 +- src/pages/Parent.tsx | 8 +- src/pages/ResetPassword.tsx | 3 +- src/pages/SelectMode.tsx | 3 +- src/pages/StickerBook.tsx | 5 +- .../components/homePage/SideMenu.tsx | 5 +- src/teachers-module/pages/ReqEditSchool.tsx | 3 +- src/utility/logger.ts | 14 +- src/utility/routerLocation.ts | 154 ++++++++++++++++++ src/utility/schoolUtil.ts | 17 +- src/utility/util.ts | 52 +++--- 36 files changed, 309 insertions(+), 109 deletions(-) create mode 100644 src/utility/routerLocation.ts diff --git a/src/App.tsx b/src/App.tsx index 20053ccb82..ead281ba65 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,7 @@ */ import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; -import { IonReactRouter } from '@ionic/react-router'; +import { IonReactHashRouter } from '@ionic/react-router'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -39,7 +39,6 @@ import './theme/variables.css'; import './App.css'; import React from 'react'; -import { BASE_NAME } from './common/constants'; import TermsGate from './components/termsandconditons/TermsGate'; import { HardwareBackButtonHandler } from './common/backButtonRegistry'; import { useAppSelector } from './redux/hooks'; @@ -78,7 +77,7 @@ const App: React.FC = () => { return ( - + { showBreakToast={usageLimit.showToast} onDismissBreakToast={() => usageLimit.setShowToast(false)} /> - + ); }; diff --git a/src/ProtectedRoute.tsx b/src/ProtectedRoute.tsx index e935ae5ff6..11c78639b1 100644 --- a/src/ProtectedRoute.tsx +++ b/src/ProtectedRoute.tsx @@ -6,6 +6,7 @@ import Loading from './components/Loading'; import { ServiceConfig } from './services/ServiceConfig'; import { logAuthDebug } from './utility/authDebug'; import { isRecoverableStorageError } from './utility/recoverableStorageError'; +import { getAppPathname } from './utility/routerLocation'; type ProtectedRouteProps = RouteProps & { children: ReactNode; @@ -48,7 +49,7 @@ export default function ProtectedRoute({ reason: !isUserLoggedIn ? 'is_user_logged_in_false' : 'current_user_missing', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); } @@ -66,7 +67,7 @@ export default function ProtectedRoute({ source: 'ProtectedRoute.checkAuth', reason: 'recoverable_auth_dependency_error', attempt, - from_page: window.location.pathname, + from_page: getAppPathname(), }, ); lifecycle.timeoutId = window.setTimeout(() => { @@ -77,7 +78,7 @@ export default function ProtectedRoute({ logAuthDebug('ProtectedRoute redirecting to login after auth error.', { source: 'ProtectedRoute.checkAuth', reason: 'auth_check_exception', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); if (lifecycle.cancelled) return; diff --git a/src/analytics/clickUtil.ts b/src/analytics/clickUtil.ts index c1b4207299..7a8baeb583 100644 --- a/src/analytics/clickUtil.ts +++ b/src/analytics/clickUtil.ts @@ -2,6 +2,7 @@ import { Util } from '../utility/util'; import { EVENTS, PAGES } from '../common/constants'; import { RoleType } from '../interface/modelInterfaces'; import { SupabaseAuth } from '../services/auth/SupabaseAuth'; +import { getAppHref, getAppPathname } from '../utility/routerLocation'; const storedStudent: { id?: string; @@ -59,7 +60,7 @@ const handleClick = async (event: MouseEvent) => { if (textContent) break; target = target.parentElement as HTMLElement; } - if (PAGES.EDIT_STUDENT === window.location.pathname) { + if (PAGES.EDIT_STUDENT === getAppPathname()) { textContent = target .getAttribute('src') ?.trim() @@ -100,9 +101,9 @@ const handleClick = async (event: MouseEvent) => { user_type: storedStudent.type, click_value: textContent, click_identifier: id || className || 'null', - page_name: window.location.pathname.replace('/', ''), - page_path: window.location.pathname, - complete_path: window.location.href, + page_name: getAppPathname().replace('/', ''), + page_path: getAppPathname(), + complete_path: getAppHref(), action_type: event.type, }; diff --git a/src/analytics/profileClickUtil.ts b/src/analytics/profileClickUtil.ts index ec5be90a3d..b37c7ca2d5 100644 --- a/src/analytics/profileClickUtil.ts +++ b/src/analytics/profileClickUtil.ts @@ -2,6 +2,7 @@ import { Util } from '../utility/util'; import { EVENTS } from '../common/constants'; import { RoleType } from '../interface/modelInterfaces'; import { ServiceConfig } from '../services/ServiceConfig'; +import { getAppHref, getAppPathname } from '../utility/routerLocation'; const storedStudent: { id?: string; @@ -92,9 +93,9 @@ export const logProfileClick = async (event: React.MouseEvent) => { user_gender: storedStudent.gender, user_type: storedStudent.type, click_identifier: id || className || 'null', - page_name: window.location.pathname.replace('/', ''), - page_path: window.location.pathname, - complete_path: window.location.href, + page_name: getAppPathname().replace('/', ''), + page_path: getAppPathname(), + complete_path: getAppHref(), action_type: event.type, click_value: textContent, input_value: (event.target as HTMLInputElement).value || 'null', diff --git a/src/common/backButtonRegistry.ts b/src/common/backButtonRegistry.ts index 6ab2a83dfc..7f7f4a269f 100644 --- a/src/common/backButtonRegistry.ts +++ b/src/common/backButtonRegistry.ts @@ -8,6 +8,7 @@ import { } from 'react'; import { Capacitor } from '@capacitor/core'; import { App as CapApp } from '@capacitor/app'; +import { getAppPathname } from '../utility/routerLocation'; export type BackButtonHandler = () => boolean | void | Promise; @@ -30,7 +31,7 @@ const normalizePath = (path: string) => { const getCurrentPath = () => { if (typeof window === 'undefined') return '/'; - return normalizePath(window.location?.pathname || '/'); + return normalizePath(getAppPathname()); }; const isActiveForPath = (record: BackButtonRecord, path: string) => { diff --git a/src/components/coloring/ColorPalette.tsx b/src/components/coloring/ColorPalette.tsx index d44e1189f4..ac8a5d49a3 100644 --- a/src/components/coloring/ColorPalette.tsx +++ b/src/components/coloring/ColorPalette.tsx @@ -1,6 +1,7 @@ import './ColorPalette.css'; import { Util } from '../../utility/util'; import { EVENTS } from '../../common/constants'; +import { getAppPathname } from '../../utility/routerLocation'; type Props = { selected: string; @@ -49,7 +50,7 @@ export default function ColorPalette({ selected, onSelect }: Props) { Util.logEvent(EVENTS.PAINT_COLOR_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, color: c, - page_path: window.location.pathname, + page_path: getAppPathname(), }); onSelect(c); }} diff --git a/src/components/coloring/ColoringBoard.tsx b/src/components/coloring/ColoringBoard.tsx index fcc7efcae4..c5baf31749 100644 --- a/src/components/coloring/ColoringBoard.tsx +++ b/src/components/coloring/ColoringBoard.tsx @@ -16,6 +16,7 @@ import PaintTopBar from './PaintTopBar'; // import { ReactComponent as SceneSvg } from "../../assets/images/tinyfriends_original.svg"; import logger from '../../utility/logger'; import { parseSvg, ParsedSvg, sanitizeSvg } from '../common/SvgHelpers'; +import { getAppPathname } from '../../utility/routerLocation'; import { Util } from '../../utility/util'; import { ENABLE_SAVE_AND_SHARE_STICKER_BOOK, @@ -118,7 +119,7 @@ const ColoringBoard: React.FC = () => { const saveAnalyticsPayload = useMemo( () => ({ user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), source: PAGES.COLORING_BOARD, artwork_title: artworkTitle, return_to: location.state?.returnTo ?? null, @@ -316,7 +317,7 @@ const ColoringBoard: React.FC = () => { useEffect(() => { Util.logEvent(EVENTS.PAINT_MODE_PAGE_VIEW, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), return_to: location.state?.returnTo ?? null, }); }, [location.state]); @@ -395,17 +396,17 @@ const ColoringBoard: React.FC = () => { const handleSave = () => { Util.logEvent(EVENTS.PAINT_SAVE_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), source: PAGES.COLORING_BOARD, }); Util.logEvent(EVENTS.PAINT_IMAGE_SAVED, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), source: PAGES.COLORING_BOARD, }); Util.logEvent(EVENTS.STICKER_BOOK_SAVE_CLICKED, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), source: PAGES.COLORING_BOARD, }); logger.info('save'); @@ -431,7 +432,7 @@ const ColoringBoard: React.FC = () => { onExit={() => { Util.logEvent(EVENTS.PAINT_EXIT_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), }); setShowExitConfirm(true); }} @@ -499,21 +500,21 @@ const ColoringBoard: React.FC = () => { onClose={() => { Util.logEvent(EVENTS.PAINT_EXIT_CLOSE_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), }); setShowExitConfirm(false); }} onStay={() => { Util.logEvent(EVENTS.PAINT_EXIT_STAY_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), }); setShowExitConfirm(false); }} onExit={() => { Util.logEvent(EVENTS.PAINT_EXIT_CONFIRM_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, - page_path: window.location.pathname, + page_path: getAppPathname(), }); void handleExit(); }} diff --git a/src/components/coloring/useSvgColoring.ts b/src/components/coloring/useSvgColoring.ts index 28502fba29..4606094cb7 100644 --- a/src/components/coloring/useSvgColoring.ts +++ b/src/components/coloring/useSvgColoring.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Util } from '../../utility/util'; import { EVENTS } from '../../common/constants'; +import { getAppPathname } from '../../utility/routerLocation'; export type ColoredRegions = Record; @@ -70,7 +71,7 @@ export const useSvgColoring = ( region_id: regionId, color: selectedColor, colored_count: Object.keys(next).length, - page_path: window.location.pathname, + page_path: getAppPathname(), }); return next; diff --git a/src/components/leaderboard/LeaderboardRewards.tsx b/src/components/leaderboard/LeaderboardRewards.tsx index 1711fa0f38..a86528d7a2 100644 --- a/src/components/leaderboard/LeaderboardRewards.tsx +++ b/src/components/leaderboard/LeaderboardRewards.tsx @@ -6,6 +6,10 @@ import LeaderboardBadges from './LeaderboardBadges'; import LeaderboardBonus from './LeaderboardBonus'; import './LeaderboardRewards.css'; import LeaderboardSticker from './LeaderboardSticker'; +import { + getAppSearchParams, + replaceAppUrl, +} from '../../utility/routerLocation'; const LeaderboardRewards: FC = () => { const [tabIndex, setTabIndex] = useState(LEADERBOARD_REWARD_LIST.BADGES); @@ -16,7 +20,7 @@ const LeaderboardRewards: FC = () => { setTabIndex(newValue); }; useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const rewardsTab = urlParams.get('rewards'); let currentTab = LEADERBOARD_REWARD_LIST.STICKER; if (rewardsTab) { @@ -32,9 +36,9 @@ const LeaderboardRewards: FC = () => { useEffect(() => { // Update URL when tabIndex changes if (tabIndex) { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('rewards', tabIndex.toLowerCase()); - window.history.replaceState({}, '', newUrl.toString()); + const nextParams = getAppSearchParams(); + nextParams.set('rewards', tabIndex.toLowerCase()); + replaceAppUrl({ search: `?${nextParams.toString()}` }); } }, [tabIndex]); diff --git a/src/components/learningPathway/StickerBookPreviewModal.logic.ts b/src/components/learningPathway/StickerBookPreviewModal.logic.ts index 23d7bb3e79..68f8a439c1 100644 --- a/src/components/learningPathway/StickerBookPreviewModal.logic.ts +++ b/src/components/learningPathway/StickerBookPreviewModal.logic.ts @@ -16,6 +16,7 @@ import { import { Util } from '../../utility/util'; import logger from '../../utility/logger'; import { AudioUtil } from '../../utility/AudioUtil'; +import { getAppPathname } from '../../utility/routerLocation'; import { fetchStickerBookSvgText } from '../../utility/stickerBookAssets'; import { useStickerBookSave } from '../../hooks/useStickerBookSave'; import { resolveStickerBookSvgUrl } from '../../utility/stickerBookAssets'; @@ -173,7 +174,7 @@ export const useStickerBookPreviewModalLogic = ({ book_title: data.stickerBookTitle, collected_count: data.collectedStickerIds.length, total_elements: data.totalStickerCount ?? data.collectedStickerIds.length, - page_path: window.location.pathname, + page_path: getAppPathname(), }), [data], ); @@ -778,7 +779,7 @@ export const useStickerBookPreviewModalLogic = ({ svgRaw, svgUrl: resolveStickerBookSvgUrl(data.stickerBookSvgUrl), artworkTitle: data.stickerBookTitle || t('Sticker Book'), - returnTo: window.location.pathname, + returnTo: getAppPathname(), }); }; diff --git a/src/components/parent/DeleteParentAccount.tsx b/src/components/parent/DeleteParentAccount.tsx index afeba08ae6..7452162f0b 100644 --- a/src/components/parent/DeleteParentAccount.tsx +++ b/src/components/parent/DeleteParentAccount.tsx @@ -11,6 +11,7 @@ import { Capacitor } from '@capacitor/core'; import { Browser } from '@capacitor/browser'; import Loading from '../Loading'; import { logAuthDebug } from '../../utility/authDebug'; +import { getAppPathname } from '../../utility/routerLocation'; const DeleteParentAccount: React.FC = () => { const [showDialogBox, setShowDialogBox] = useState(false); @@ -45,7 +46,7 @@ const DeleteParentAccount: React.FC = () => { logAuthDebug('Navigating to login after parent account deletion.', { source: 'DeleteParentAccount.ondelete', reason: 'account_deleted_navigate_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, user_id: user?.id, }); diff --git a/src/components/parent/ParentLogout.tsx b/src/components/parent/ParentLogout.tsx index 9f3b52a9ad..ce8673ef2f 100644 --- a/src/components/parent/ParentLogout.tsx +++ b/src/components/parent/ParentLogout.tsx @@ -16,6 +16,7 @@ import { Capacitor } from '@capacitor/core'; import { Util } from '../../utility/util'; import { ClearCacheData } from './DataClear'; import { logAuthDebug } from '../../utility/authDebug'; +import { getAppPathname } from '../../utility/routerLocation'; const ParentLogout: React.FC<{}> = ({}) => { const [showDialogBox, setShowDialogBox] = useState(false); @@ -36,7 +37,7 @@ const ParentLogout: React.FC<{}> = ({}) => { logAuthDebug('Navigating to login after parent logout.', { source: 'ParentLogout.onSignOut', reason: 'logout_complete_navigate_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/components/profileDetails/ProfileDetails.tsx b/src/components/profileDetails/ProfileDetails.tsx index 7efb8f56bf..81c73fb3f1 100644 --- a/src/components/profileDetails/ProfileDetails.tsx +++ b/src/components/profileDetails/ProfileDetails.tsx @@ -34,6 +34,10 @@ import { reinitializeHardwareBackButton, } from '../../common/backButtonRegistry'; import logger from '../../utility/logger'; +import { + getAppPathname, + getAppSearchParams, +} from '../../utility/routerLocation'; import { schoolUtil } from '../../utility/schoolUtil'; import { updateLocalAttributes, @@ -246,7 +250,7 @@ const ProfileDetails = () => { }; const withContinueIfNeeded = (base: string) => { - const url = new URLSearchParams(window.location.search); + const url = getAppSearchParams(); if (!url.has(CONTINUE)) return base; return base.includes('?') ? `${base}&${CONTINUE}=true` @@ -262,7 +266,7 @@ const ProfileDetails = () => { try { // Determine Mode based on Live Pathname (Not State) - const currentPath = window.location.pathname; + const currentPath = getAppPathname(); const isEditMode = currentPath.startsWith(PAGES.EDIT_STUDENT); // EDIT MODE Logic @@ -395,7 +399,7 @@ const ProfileDetails = () => { gender, language_id: languageId, variation, - page_path: window.location.pathname, + page_path: getAppPathname(), action_type: ACTION_TYPES.PROFILE_UPDATED, }); @@ -436,7 +440,7 @@ const ProfileDetails = () => { gender, language_id: languageId, variation, - page_path: window.location.pathname, + page_path: getAppPathname(), action_type: ACTION_TYPES.PROFILE_CREATED, }); @@ -507,7 +511,7 @@ const ProfileDetails = () => { user_id: user?.id, name: fullName, variation, - page_path: window.location.pathname, + page_path: getAppPathname(), action_type: ACTION_TYPES.PROFILE_CREATED, }); diff --git a/src/components/stickerBook/StickerBookBoard.tsx b/src/components/stickerBook/StickerBookBoard.tsx index 7ad0d4b545..723416c3cd 100644 --- a/src/components/stickerBook/StickerBookBoard.tsx +++ b/src/components/stickerBook/StickerBookBoard.tsx @@ -13,6 +13,7 @@ import { import NewBackButton from '../common/NewBackButton'; import './StickerBookBoard.css'; import logger from '../../utility/logger'; +import { getAppPathname } from '../../utility/routerLocation'; import StickerBookActions from './StickerBookActions'; type Props = { @@ -133,7 +134,7 @@ const StickerBookBoard: React.FC = ({ Util.logEvent(EVENTS.PAINT_MODE_BUTTON_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, book_title: title, - page_path: window.location.pathname, + page_path: getAppPathname(), source: 'sticker_book', }); if (onPaint) onPaint(); @@ -143,13 +144,13 @@ const StickerBookBoard: React.FC = ({ Util.logEvent(EVENTS.PAINT_SAVE_TAP, { user_id: Util.getCurrentStudent()?.id ?? null, book_title: title, - page_path: window.location.pathname, + page_path: getAppPathname(), source: 'sticker_book', }); Util.logEvent(EVENTS.PAINT_IMAGE_SAVED, { user_id: Util.getCurrentStudent()?.id ?? null, book_title: title, - page_path: window.location.pathname, + page_path: getAppPathname(), source: 'sticker_book', }); if (onSave) onSave(); diff --git a/src/ops-console/components/SchoolDetailsComponents/SchoolNotes.tsx b/src/ops-console/components/SchoolDetailsComponents/SchoolNotes.tsx index 5368b33ae2..18f85fdd78 100644 --- a/src/ops-console/components/SchoolDetailsComponents/SchoolNotes.tsx +++ b/src/ops-console/components/SchoolDetailsComponents/SchoolNotes.tsx @@ -8,6 +8,7 @@ import { NOTES_UPDATED_EVENT } from '../../../common/constants'; import TableSortLabel from '@mui/material/TableSortLabel'; import { Pagination } from '@mui/material'; import logger from '../../../utility/logger'; +import { getAppPathname } from '../../../utility/routerLocation'; type ApiNote = { id: string; @@ -47,7 +48,7 @@ function parseDateForDisplay(isoOrString?: string) { function detectSchoolIdFromUrl(): string | null { try { - const parts = window.location.pathname.split('/').filter(Boolean); + const parts = getAppPathname().split('/').filter(Boolean); return parts[parts.length - 1]?.split('?')[0] ?? null; } catch { return null; diff --git a/src/ops-console/components/Sidebar.tsx b/src/ops-console/components/Sidebar.tsx index 29d45a2b5b..39c0af7e20 100644 --- a/src/ops-console/components/Sidebar.tsx +++ b/src/ops-console/components/Sidebar.tsx @@ -34,6 +34,7 @@ import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import { AuthState } from '../../redux/slices/auth/authSlice'; import { logAuthDebug } from '../../utility/authDebug'; +import { getAppPathname } from '../../utility/routerLocation'; interface SidebarProps { name: string; @@ -175,7 +176,7 @@ const Sidebar: React.FC = ({ name, email, photo }) => { logAuthDebug('Navigating to login after ops console logout.', { source: 'OpsSidebar.onSignOut', reason: 'logout_complete_navigate_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/pages/AppLangSelection.tsx b/src/pages/AppLangSelection.tsx index a6d5037054..c06703d36e 100644 --- a/src/pages/AppLangSelection.tsx +++ b/src/pages/AppLangSelection.tsx @@ -8,6 +8,7 @@ import i18n from '../i18n'; import NextButton from '../components/common/NextButton'; import DropDown from '../components/DropDown'; import { logAuthDebug } from '../utility/authDebug'; +import { getAppPathname } from '../utility/routerLocation'; const AppLangSelection: React.FC = () => { const history = useHistory(); @@ -49,7 +50,7 @@ const AppLangSelection: React.FC = () => { logAuthDebug('Navigating to login from language selection screen.', { source: 'AppLangSelection.handleNextClick', reason: 'language_selected_continue_to_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/pages/DisplayStudents.tsx b/src/pages/DisplayStudents.tsx index 9a4010cfc5..7c72a3e54a 100644 --- a/src/pages/DisplayStudents.tsx +++ b/src/pages/DisplayStudents.tsx @@ -18,6 +18,7 @@ import InlineSvg from '../components/InlineSvg'; import { updateLocalAttributes, useGbContext } from '../growthbook/Growthbook'; import { ServiceConfig } from '../services/ServiceConfig'; import logger from '../utility/logger'; +import { getAppSearch } from '../utility/routerLocation'; import { schoolUtil } from '../utility/schoolUtil'; import { Util } from '../utility/util'; import BrandLogoIcon from './assets/brandLogoIcon.svg?raw'; @@ -108,7 +109,7 @@ const DisplayStudents: FC<{}> = () => { from: history.location.pathname, }); } else { - history.replace(PAGES.HOME + window.location.search); + history.replace(PAGES.HOME + getAppSearch()); } }; return ( diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 2b166f2aff..306acdd0d1 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -50,6 +50,7 @@ import { HomeworkPathwayLesson, } from '../components/assignment/homeworkPathwayHelpers'; import { replaceWithNavigationTarget } from '../helper/navigation/NavigationHandler'; +import { getAppSearchParams } from '../utility/routerLocation'; const localData: any = {}; @@ -297,7 +298,7 @@ const Home: FC = () => { }; updateAtb(); - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); if (urlParams.get('page') === PAGES.JOIN_CLASS) { setCurrentHeader(HOMEHEADERLIST.ASSIGNMENT); setTimeout(() => { diff --git a/src/pages/HotUpdate.tsx b/src/pages/HotUpdate.tsx index 85039898e7..e130de5232 100644 --- a/src/pages/HotUpdate.tsx +++ b/src/pages/HotUpdate.tsx @@ -8,6 +8,7 @@ import { useFeatureValue, useFeatureIsOn } from '@growthbook/growthbook-react'; import { ServiceConfig } from '../services/ServiceConfig'; import Loading from '../components/Loading'; import { logAuthDebug } from '../utility/authDebug'; +import { getAppPathname } from '../utility/routerLocation'; const HotUpdate: FC<{}> = () => { const history = useHistory(); @@ -54,7 +55,7 @@ const HotUpdate: FC<{}> = () => { logAuthDebug('Navigating to login from hot update entry point.', { source: 'HotUpdate.push', reason: 'language_not_set', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/pages/Leaderboard.tsx b/src/pages/Leaderboard.tsx index c6fd2754a7..d8a1690922 100644 --- a/src/pages/Leaderboard.tsx +++ b/src/pages/Leaderboard.tsx @@ -33,6 +33,7 @@ import { App } from '@capacitor/app'; import { updateLocalAttributes, useGbContext } from '../growthbook/Growthbook'; import DialogBoxButtons from '../components/parent/DialogBoxButtons'; import DebugMode from '../teachers-module/components/DebugMode'; +import { getAppSearchParams, replaceAppUrl } from '../utility/routerLocation'; const emptyLeaderboardInfo = (): LeaderboardInfo => ({ weekly: [], @@ -96,7 +97,7 @@ const Leaderboard: React.FC = () => { const api = ServiceConfig.getI().apiHandler; const auth = ServiceConfig.getI().authHandler; const history = useHistory(); - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const isRewardPage = urlParams.get('tab') === LEADERBOARDHEADERLIST.REWARDS.toLowerCase(); const { setGbUpdated } = useGbContext(); @@ -133,7 +134,7 @@ const Leaderboard: React.FC = () => { setIsLoading(true); Util.loadBackgroundImage(); inti(); - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const rewardsTab = urlParams.get('tab'); let currentTab = LEADERBOARDHEADERLIST.LEADERBOARD; if (rewardsTab) { @@ -147,9 +148,9 @@ const Leaderboard: React.FC = () => { useEffect(() => { // Update URL when tabIndex changes if (tabIndex) { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('tab', tabIndex.toLowerCase()); - window.history.replaceState({}, '', newUrl.toString()); + const nextParams = getAppSearchParams(); + nextParams.set('tab', tabIndex.toLowerCase()); + replaceAppUrl({ search: `?${nextParams.toString()}` }); } }, [tabIndex]); diff --git a/src/pages/LidoPlayer.tsx b/src/pages/LidoPlayer.tsx index 9d5ab2c97b..2d18af3177 100644 --- a/src/pages/LidoPlayer.tsx +++ b/src/pages/LidoPlayer.tsx @@ -47,6 +47,7 @@ import { getBundleZipUrlsForEnv, REMOTE_CONFIG_KEYS, } from '../services/RemoteConfig'; +import { getAppPathname, getAppSearchParams } from '../utility/routerLocation'; const HOMEWORK_REWARD_COMPLETED_INDEX_KEY = 'homework_reward_completed_index'; const PENDING_HOMEWORK_REWARD_TRANSITION_KEY = @@ -135,7 +136,7 @@ const LidoPlayer: FC = () => { SOURCE.LEARNING_PATHWAY_HOME_PAL, SOURCE.INITIAL_ASSESSMENT, ].includes(source); - const urlSearchParams = new URLSearchParams(window.location.search); + const urlSearchParams = getAppSearchParams(); const lessonId = urlSearchParams.get('lessonid') ?? state?.lessonId; const assignmentType = state?.assignment?.type || 'self-played'; const playedFrom = localStorage.getItem(CURRENT_HEADER); @@ -521,7 +522,7 @@ const LidoPlayer: FC = () => { if (isExitingRef.current) return; isExitingRef.current = true; localStorage.removeItem(LIDO_SCORES_KEY); - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const fromPath: string = state?.from ?? PAGES.HOME; const returnState = { ...(state?.returnState ?? state), @@ -1328,7 +1329,7 @@ const LidoPlayer: FC = () => { useEffect(() => { const unregister = registerBackButtonHandler( () => { - if (window.location.pathname !== PAGES.LIDO_PLAYER) return false; + if (getAppPathname() !== PAGES.LIDO_PLAYER) return false; push(); return true; }, @@ -1364,7 +1365,7 @@ const LidoPlayer: FC = () => { if (typeof window !== 'undefined') { window.__LIDO_COMMON_AUDIO_PATH__ = undefined; } - const urlSearchParams = new URLSearchParams(window.location.search); + const urlSearchParams = getAppSearchParams(); const lessonToDownload = lessonDetail; const lessonId = Util.getLessonBundleId(lessonToDownload) ?? diff --git a/src/pages/LiveQuizGame.tsx b/src/pages/LiveQuizGame.tsx index eda01fff20..1250c37ec5 100644 --- a/src/pages/LiveQuizGame.tsx +++ b/src/pages/LiveQuizGame.tsx @@ -21,11 +21,12 @@ import { t } from 'i18next'; import { Capacitor } from '@capacitor/core'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; import { useGrowthBook } from '@growthbook/growthbook-react'; +import { getAppSearchParams } from '../utility/routerLocation'; const LiveQuizGame: FC = () => { const api = ServiceConfig.getI().apiHandler; const history = useHistory(); - const urlSearchParams = new URLSearchParams(window.location.search); + const urlSearchParams = getAppSearchParams(); const paramLiveRoomId = urlSearchParams.get('liveRoomId'); const [roomDoc, setRoomDoc] = useState>(); const [isTimeOut, setIsTimeOut] = useState(false); @@ -131,7 +132,7 @@ const LiveQuizGame: FC = () => { }; const push = () => { - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const fromPath: string = state?.from ?? PAGES.HOME; const returnState = { ...(state?.returnState ?? state), diff --git a/src/pages/LiveQuizLeaderBoard.tsx b/src/pages/LiveQuizLeaderBoard.tsx index 379a2ac36e..4158b2fd3c 100644 --- a/src/pages/LiveQuizLeaderBoard.tsx +++ b/src/pages/LiveQuizLeaderBoard.tsx @@ -7,6 +7,7 @@ import { PAGES, TableTypes } from '../common/constants'; import { t } from 'i18next'; import { useHistory } from 'react-router'; import NextButton from '../components/common/NextButton'; +import { getAppSearchParams } from '../utility/routerLocation'; import { Util } from '../utility/util'; import logger from '../utility/logger'; @@ -29,7 +30,7 @@ const LiveQuizLeaderBoard: React.FC = () => { const [students, setStudents] = useState( new Map>(), ); - const urlSearchParams = new URLSearchParams(window.location.search); + const urlSearchParams = getAppSearchParams(); const paramLiveRoomId = urlSearchParams.get('liveRoomId') ?? ''; const api = ServiceConfig.getI().apiHandler; const history = useHistory(); diff --git a/src/pages/LiveQuizRoom.tsx b/src/pages/LiveQuizRoom.tsx index 221d1e9963..323dcbb507 100644 --- a/src/pages/LiveQuizRoom.tsx +++ b/src/pages/LiveQuizRoom.tsx @@ -12,6 +12,7 @@ import { useOnlineOfflineErrorMessageHandler } from '../common/onlineOfflineErro import BackButton from '../components/common/BackButton'; import SkeltonLoading from '../components/SkeltonLoading'; import { ServiceConfig } from '../services/ServiceConfig'; +import { getAppSearchParams } from '../utility/routerLocation'; const LiveQuizRoom: React.FC = () => { const [students, setStudents] = useState( new Map>(), @@ -26,7 +27,7 @@ const LiveQuizRoom: React.FC = () => { useState>(); const api = ServiceConfig.getI().apiHandler; const history = useHistory(); - const urlSearchParams = new URLSearchParams(window.location.search); + const urlSearchParams = getAppSearchParams(); const paramAssignmentId = urlSearchParams.get('assignmentId') ?? ''; const [isDownloaded, setIsDownloaded] = useState(false); const [isJoining, setIsJoining] = useState(false); diff --git a/src/pages/LiveQuizRoomResult.tsx b/src/pages/LiveQuizRoomResult.tsx index 5a812f6042..7632dc53e8 100644 --- a/src/pages/LiveQuizRoomResult.tsx +++ b/src/pages/LiveQuizRoomResult.tsx @@ -10,6 +10,7 @@ import { GiCrown } from 'react-icons/gi'; import { t } from 'i18next'; import { IonPage } from '@ionic/react'; import logger from '../utility/logger'; +import { getAppSearchParams } from '../utility/routerLocation'; const LiveQuizRoomResult: React.FC = () => { const [topThreeStudents, setTopThreeStudents] = useState< @@ -29,7 +30,7 @@ const LiveQuizRoomResult: React.FC = () => { [], ); const [isCongratsVisible, setCongratsVisible] = useState(true); - const urlSearchParams = new URLSearchParams(window.location.search); + const urlSearchParams = getAppSearchParams(); const paramLiveRoomId = urlSearchParams.get('liveRoomId') ?? ''; const api = ServiceConfig.getI().apiHandler; diff --git a/src/pages/Parent.tsx b/src/pages/Parent.tsx index 44b293a862..b9e22de6d9 100644 --- a/src/pages/Parent.tsx +++ b/src/pages/Parent.tsx @@ -33,6 +33,7 @@ import { Box } from '@mui/material'; import { useHistory, useLocation } from 'react-router-dom'; import CustomAppBar from '../components/studentProgress/CustomAppBar'; import { Util } from '../utility/util'; +import { getAppPathname } from '../utility/routerLocation'; import { schoolUtil } from '../utility/schoolUtil'; import { RoleType } from '../interface/modelInterfaces'; import { setUser } from '../redux/slices/auth/authSlice'; @@ -346,9 +347,9 @@ const Parent: React.FC = () => { history.push({ pathname: PAGES.TERMS_AND_CONDITIONS, state: { - from: window.location.pathname, + from: getAppPathname(), returnLocation: { - pathname: window.location.pathname, + pathname: getAppPathname(), state: { activeTab: 'settings', }, @@ -362,6 +363,7 @@ const Parent: React.FC = () => { logAuthDebug('User initiated parent logout.', { source: 'Parent.settingUI.handleSignOut', reason: 'parent_logout_button', + from_page: getAppPathname(), }); await auth.logOut(); Util.unSubscribeToClassTopicForAllStudents(); @@ -373,7 +375,7 @@ const Parent: React.FC = () => { logAuthDebug('Navigating to login after parent logout.', { source: 'Parent.settingUI.handleSignOut', reason: 'logout_complete_navigate_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx index d9e5838eb4..48be855142 100644 --- a/src/pages/ResetPassword.tsx +++ b/src/pages/ResetPassword.tsx @@ -8,6 +8,7 @@ import './ResetPassword.css'; import { t } from 'i18next'; import logger from '../utility/logger'; import { logAuthDebug } from '../utility/authDebug'; +import { getAppPathname } from '../utility/routerLocation'; const ResetPassword: React.FC = () => { const location = useLocation(); @@ -38,7 +39,7 @@ const ResetPassword: React.FC = () => { logAuthDebug('Navigating to login after successful password reset.', { source: 'ResetPassword.handlePasswordReset', reason: 'password_reset_success', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.push(PAGES.LOGIN); diff --git a/src/pages/SelectMode.tsx b/src/pages/SelectMode.tsx index 6df066e11f..c12e6593e8 100644 --- a/src/pages/SelectMode.tsx +++ b/src/pages/SelectMode.tsx @@ -53,6 +53,7 @@ import { isTeacherAppRole, resolveTeacherAppModeForRole, } from '../utility/roleUtil'; +import { getAppSearchParams } from '../utility/routerLocation'; import { schoolUtil } from '../utility/schoolUtil'; import { Util } from '../utility/util'; import BrandLogoIcon from './assets/brandLogoIcon.svg?raw'; @@ -296,7 +297,7 @@ const SelectMode: FC = () => { } }; const init = async () => { - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const setTab = urlParams.get('tab'); const currentMode = await schoolUtil.getCurrMode(); const isSchoolMode = diff --git a/src/pages/StickerBook.tsx b/src/pages/StickerBook.tsx index c11b78542f..e05e8c5c3d 100644 --- a/src/pages/StickerBook.tsx +++ b/src/pages/StickerBook.tsx @@ -19,6 +19,7 @@ import StickerBookSaveModal from '../components/stickerBook/StickerBookSaveModal import StickerBookToast from '../components/stickerBook/StickerBookToast'; import { t } from 'i18next'; import NewBackButton from '../components/common/NewBackButton'; +import { getAppPathname } from '../utility/routerLocation'; import { fetchStickerBookSvgText, resolveStickerBookSvgUrl, @@ -203,7 +204,7 @@ const StickerBook: React.FC = () => { book_title: selectedBook?.title ?? null, collected_count: collectedStickers.length, total_elements: allStickerIds.length, - page_path: window.location.pathname, + page_path: getAppPathname(), }), [selectedBook, collectedStickers.length, allStickerIds.length], ); @@ -286,7 +287,7 @@ const StickerBook: React.FC = () => { total_elements: total, colored_elements: colored, uncolored_elements: uncolored, - page_path: window.location.pathname, + page_path: getAppPathname(), }); }, [selectedBook, allStickerIds, collectedStickers]); diff --git a/src/teachers-module/components/homePage/SideMenu.tsx b/src/teachers-module/components/homePage/SideMenu.tsx index 8b88342929..cdf61be411 100644 --- a/src/teachers-module/components/homePage/SideMenu.tsx +++ b/src/teachers-module/components/homePage/SideMenu.tsx @@ -25,6 +25,7 @@ import { RootState } from '../../../redux/store'; import { ServiceConfig } from '../../../services/ServiceConfig'; import { logAuthDebug } from '../../../utility/authDebug'; import logger from '../../../utility/logger'; +import { getAppPathname } from '../../../utility/routerLocation'; import { Util } from '../../../utility/util'; import ClassSection from './ClassSection'; import ProfileSection from './ProfileDetail'; @@ -101,7 +102,7 @@ const SideMenu: React.FC<{ const api = ServiceConfig.getI()?.apiHandler; const getSwitchToKidsAppSourceScreen = (): SwitchToKidsAppSourceScreen => { - if (window.location.pathname.startsWith(PAGES.SIDEBAR_PAGE)) { + if (getAppPathname().startsWith(PAGES.SIDEBAR_PAGE)) { return SWITCH_TO_KIDS_APP_SOURCE_SCREEN.OPS_CONSOLE; } return SWITCH_TO_KIDS_APP_SOURCE_SCREEN.TEACHER_DASHBOARD; @@ -335,7 +336,7 @@ const SideMenu: React.FC<{ logAuthDebug('Navigating to login after teacher side-menu logout.', { source: 'TeacherSideMenu.onSignOut', reason: 'logout_complete_navigate_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/teachers-module/pages/ReqEditSchool.tsx b/src/teachers-module/pages/ReqEditSchool.tsx index 109516e6cb..9d58600dec 100644 --- a/src/teachers-module/pages/ReqEditSchool.tsx +++ b/src/teachers-module/pages/ReqEditSchool.tsx @@ -21,6 +21,7 @@ import { schoolUtil } from '../../utility/schoolUtil'; import { useOnlineOfflineErrorMessageHandler } from '../../common/onlineOfflineErrorMessageHandler'; import logger from '../../utility/logger'; import { logAuthDebug } from '../../utility/authDebug'; +import { getAppPathname } from '../../utility/routerLocation'; interface LocationState { school?: SchoolWithRole['school']; role?: RoleType; @@ -129,7 +130,7 @@ const ReqEditSchool: React.FC = () => { logAuthDebug('Navigating to login after teacher school-request logout.', { source: 'ReqEditSchool.onSignOut', reason: 'logout_complete_navigate_login', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); history.replace(PAGES.LOGIN); diff --git a/src/utility/logger.ts b/src/utility/logger.ts index 7ba91ecedc..b2036234d5 100644 --- a/src/utility/logger.ts +++ b/src/utility/logger.ts @@ -4,6 +4,7 @@ import { NativeBridgePayload, sanitizeNativeBridgePayload, } from './safeNativeBridgePayload'; +import { getAppPathname } from './routerLocation'; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; type LogMethod = 'debug' | 'info' | 'warn' | 'error'; @@ -54,14 +55,15 @@ const IS_NATIVE = !!(window as any).Capacitor && (window as any).Capacitor.getPlatform() !== 'web'; -let cachedPage: string | undefined = HAS_WINDOW - ? window.location.pathname - : undefined; +let cachedPage: string | undefined = HAS_WINDOW ? getAppPathname() : undefined; if (HAS_WINDOW) { - window.addEventListener('popstate', () => { - cachedPage = window.location.pathname; - }); + const updateCachedPage = () => { + cachedPage = getAppPathname(); + }; + + window.addEventListener('popstate', updateCachedPage); + window.addEventListener('hashchange', updateCachedPage); } /** diff --git a/src/utility/routerLocation.ts b/src/utility/routerLocation.ts new file mode 100644 index 0000000000..8b11ac6ce1 --- /dev/null +++ b/src/utility/routerLocation.ts @@ -0,0 +1,154 @@ +import { createPath, parsePath } from 'history'; + +import { BASE_NAME } from '../common/constants'; + +type AppLocationParts = { + pathname: string; + search: string; + hash: string; +}; + +const normalizePathname = (pathname: string) => { + const trimmed = pathname.replace(/\/+$/, ''); + return trimmed || '/'; +}; + +const normalizeSearch = (search: string) => { + if (!search || search === '?') { + return ''; + } + + return search.startsWith('?') ? search : `?${search}`; +}; + +const normalizeHash = (hash: string) => { + if (!hash || hash === '#') { + return ''; + } + + return hash.startsWith('#') ? hash : `#${hash}`; +}; + +const normalizeBasename = (basename: string) => { + if (!basename || basename === '/') { + return ''; + } + + const trimmed = basename.replace(/\/+$/, ''); + return trimmed.startsWith('/') ? trimmed : `/${trimmed}`; +}; + +const hasBasename = (path: string, basename: string) => + path.toLowerCase().indexOf(basename.toLowerCase()) === 0 && + '/?#'.includes(path.charAt(basename.length)); + +const stripBasename = (pathname: string) => { + const basename = normalizeBasename(BASE_NAME); + if (!basename || !hasBasename(pathname, basename)) { + return pathname; + } + + return pathname.slice(basename.length) || '/'; +}; + +const addBasename = (pathname: string) => { + const basename = normalizeBasename(BASE_NAME); + if (!basename) { + return pathname; + } + + return pathname === '/' ? basename : `${basename}${pathname}`; +}; + +const isHashRoutedUrl = () => + typeof window !== 'undefined' && window.location.hash.startsWith('#/'); + +const getHashRoutePath = () => { + if (typeof window === 'undefined') { + return '/'; + } + + const rawHash = window.location.hash.replace(/^#/, ''); + if (!rawHash) { + return '/'; + } + + return rawHash.startsWith('/') ? rawHash : `/${rawHash}`; +}; + +const readCurrentAppLocation = (): AppLocationParts => { + if (typeof window === 'undefined') { + return { pathname: '/', search: '', hash: '' }; + } + + const parsed = isHashRoutedUrl() + ? parsePath(getHashRoutePath()) + : parsePath( + `${window.location.pathname}${window.location.search}${window.location.hash}`, + ); + + return { + pathname: normalizePathname( + isHashRoutedUrl() + ? parsed.pathname || '/' + : stripBasename(parsed.pathname || '/'), + ), + search: normalizeSearch(parsed.search || ''), + hash: normalizeHash(parsed.hash || ''), + }; +}; + +const withDefaults = ( + nextLocation: Partial = {}, +): AppLocationParts => { + const current = readCurrentAppLocation(); + + return { + pathname: normalizePathname(nextLocation.pathname ?? current.pathname), + search: normalizeSearch(nextLocation.search ?? current.search), + hash: normalizeHash(nextLocation.hash ?? current.hash), + }; +}; + +export const getAppLocation = () => readCurrentAppLocation(); + +export const getAppPathname = () => readCurrentAppLocation().pathname; + +export const getAppSearch = () => readCurrentAppLocation().search; + +export const getAppHash = () => readCurrentAppLocation().hash; + +export const getAppSearchParams = () => new URLSearchParams(getAppSearch()); + +export const getAppPath = () => createPath(readCurrentAppLocation()); + +export const buildAppUrl = (nextLocation: Partial = {}) => { + if (typeof window === 'undefined') { + return new URL('http://localhost/'); + } + + const location = withDefaults(nextLocation); + const url = new URL(window.location.href); + + if (isHashRoutedUrl()) { + url.hash = createPath({ + pathname: location.pathname, + search: location.search, + hash: location.hash, + }); + } else { + url.pathname = addBasename(location.pathname); + url.search = location.search; + url.hash = location.hash; + } + + return url; +}; + +export const getAppHref = () => buildAppUrl().toString(); + +export const replaceAppUrl = (nextLocation: Partial) => { + const url = buildAppUrl(nextLocation); + window.history.replaceState(window.history.state, '', url.toString()); + return url; +}; diff --git a/src/utility/schoolUtil.ts b/src/utility/schoolUtil.ts index 24569055f2..b0be7a6a7b 100644 --- a/src/utility/schoolUtil.ts +++ b/src/utility/schoolUtil.ts @@ -14,6 +14,7 @@ import { store } from '../redux/store'; import { ServiceConfig } from '../services/ServiceConfig'; import { logAuthDebug } from './authDebug'; import logger from './logger'; +import { getAppPathname, replaceAppUrl } from './routerLocation'; import { Util } from './util'; export class schoolUtil { @@ -142,14 +143,10 @@ export class schoolUtil { logAuthDebug('School relogin succeeded, redirecting to select mode.', { source: 'schoolUtil.trySchoolRelogin', reason: 'school_relogin_success', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.SELECT_MODE, }); - window.history.replaceState( - window.history.state, - '', - PAGES.SELECT_MODE.toString(), - ); + replaceAppUrl({ pathname: PAGES.SELECT_MODE, search: '', hash: '' }); return true; } else { @@ -157,14 +154,10 @@ export class schoolUtil { logAuthDebug('School relogin failed, redirecting to login.', { source: 'schoolUtil.trySchoolRelogin', reason: 'school_relogin_failed_user_not_found', - from_page: window.location.pathname, + from_page: getAppPathname(), to_page: PAGES.LOGIN, }); - window.history.replaceState( - window.history.state, - '', - PAGES.LOGIN.toString(), - ); + replaceAppUrl({ pathname: PAGES.LOGIN, search: '', hash: '' }); return false; } } catch (error) { diff --git a/src/utility/util.ts b/src/utility/util.ts index c40700d29c..9628409c28 100644 --- a/src/utility/util.ts +++ b/src/utility/util.ts @@ -124,6 +124,11 @@ import logger from './logger'; import type { StickerBookModalData } from '../components/learningPathway/StickerBookPreviewModal'; import { AudioUtil } from './AudioUtil'; import { replaceWithNavigationTarget } from '../helper/navigation/NavigationHandler'; +import { + getAppPathname, + getAppSearchParams, + replaceAppUrl, +} from './routerLocation'; type LessonBundleDownloadOptions = { lessonId: string; @@ -1097,8 +1102,8 @@ export class Util { }); await FirebaseAnalytics.setScreenName({ - screenName: window.location.pathname, - nameOverride: window.location.pathname, + screenName: getAppPathname(), + nameOverride: getAppPathname(), }); await FirebaseAnalytics.logEvent({ @@ -1147,8 +1152,8 @@ export class Util { //Setting Screen Name await FirebaseAnalytics.setScreenName({ - screenName: window.location.pathname, - nameOverride: window.location.pathname, + screenName: getAppPathname(), + nameOverride: getAppPathname(), }); } @@ -1159,8 +1164,9 @@ export class Util { logger.info('[Lifecycle] App state changed', { isActive }); // Handling app state changes (reloading pages, updating URLs, etc.) - const url = new URL(window.location.toString()); - const urlParams = new URLSearchParams(window.location.search); + const currentPath = getAppPathname(); + const continueValue = getAppSearchParams().get(CONTINUE); + const urlParams = getAppSearchParams(); if (!!urlParams.get(CONTINUE)) { urlParams.delete(CONTINUE); } @@ -1172,29 +1178,37 @@ export class Util { if (isActive) { if ( Capacitor.isNativePlatform() && - url.searchParams.get(CONTINUE) === 'true' && - url.pathname !== PAGES.LOGIN && - url.pathname !== PAGES.EDIT_STUDENT + continueValue === 'true' && + currentPath !== PAGES.LOGIN && + currentPath !== PAGES.EDIT_STUDENT ) { if ( - url.pathname === PAGES.DISPLAY_SUBJECTS || - url.pathname === PAGES.DISPLAY_CHAPTERS + currentPath === PAGES.DISPLAY_SUBJECTS || + currentPath === PAGES.DISPLAY_CHAPTERS ) { - url.searchParams.set('isReload', 'true'); + urlParams.set('isReload', 'true'); } - url.searchParams.delete(CONTINUE); - window.history.replaceState(window.history.state, '', url.toString()); + urlParams.delete(CONTINUE); + replaceAppUrl({ + pathname: currentPath, + search: urlParams.toString() ? `?${urlParams.toString()}` : '', + hash: '', + }); window.location.reload(); } else { - url.searchParams.set('isReload', 'true'); - url.searchParams.delete(CONTINUE); - window.history.replaceState(window.history.state, '', url.toString()); + urlParams.set('isReload', 'true'); + urlParams.delete(CONTINUE); + replaceAppUrl({ + pathname: currentPath, + search: urlParams.toString() ? `?${urlParams.toString()}` : '', + hash: '', + }); } } }; public static setPathToBackButton(path: string, history: any) { - const url = new URLSearchParams(window.location.search); + const url = getAppSearchParams(); if (url.get(CONTINUE)) { history.replace(`${path}?${CONTINUE}=true`); } else { @@ -1780,7 +1794,7 @@ export class Util { // Determine target page for logging let destinationPage = ''; const newSearchParams = new URLSearchParams(url.search); - const currentParams = new URLSearchParams(window.location.search); + const currentParams = getAppSearchParams(); currentParams.set('classCode', newSearchParams.get('classCode') ?? ''); currentParams.set('page', PAGES.JOIN_CLASS); const currentStudent = Util.getCurrentStudent(); From 326a4371415f7ba77e7a183de6b37429cf8e0176 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Thu, 9 Jul 2026 16:46:49 +0530 Subject: [PATCH 2/7] fix(router): update stateful navigation calls for IonReactHashRouter --- src/app/AppRoutes.tsx | 3 + src/common/constants.ts | 2 + src/components/LessonCard.tsx | 44 +++++++----- src/components/ProfileMenu/ProfileMenu.tsx | 38 ++++++++-- .../ActivationLessonBanner.tsx | 20 +++--- .../assignment/HomeworkPathwayStructure.tsx | 72 +++++++++++-------- .../StickerBookPreviewModal.logic.ts | 16 +++-- src/components/parent/DataClear.tsx | 14 +++- src/components/parent/ProfileCard.tsx | 17 +++-- src/hooks/usePathwayData.ts | 40 ++++++----- src/hooks/usePathwaySVG.ts | 38 +++++----- .../SchoolOverview.tsx | 41 ++++++----- src/ops-console/components/Sidebar.tsx | 19 ++++- src/ops-console/pages/ActivitiesPage.tsx | 11 +-- src/pages/DisplayStudents.tsx | 16 +++-- src/pages/LidoPlayer.tsx | 3 +- src/pages/LiveQuizGame.tsx | 18 +++-- src/pages/LiveQuizRoom.tsx | 7 +- src/pages/StickerBook.tsx | 16 +++-- src/pages/TermsAndConditions.tsx | 11 +-- src/services/auth/SupabaseAuth.ts | 2 +- .../components/ChapterWiseLessons.tsx | 30 ++++---- .../components/CreateSchoolPrompt.tsx | 16 +++-- .../homePage/assignment/AssignScreen.tsx | 12 ++-- .../assignment/CreateSelectedAssignment.tsx | 16 ++++- .../homePage/assignment/QRAssignments.tsx | 43 ++++++----- .../homePage/assignment/ScanRedirect.tsx | 3 +- .../homePage/assignment/TeacherAssignment.tsx | 21 ++++-- .../TeacherRecommendedAssignments.tsx | 18 +++-- .../homePage/dashBoard/DashBoard.tsx | 17 +++-- .../components/library/Library.tsx | 9 ++- .../components/reports/ReportsTable.tsx | 18 +++-- .../components/schoolComponent/DetailList.tsx | 36 +++++++--- .../components/studentProfile/UserDetail.tsx | 20 ++++-- src/teachers-module/pages/AddSchoolUser.tsx | 19 +++-- src/teachers-module/pages/AddStudent.tsx | 3 +- src/teachers-module/pages/AddTeacher.tsx | 11 ++- src/teachers-module/pages/ClassProfile.tsx | 11 ++- src/teachers-module/pages/ClassUsers.tsx | 19 +++-- .../pages/DashBoardDetails.tsx | 6 +- src/teachers-module/pages/DisplayClasses.tsx | 34 +++++---- src/teachers-module/pages/DisplaySchools.tsx | 40 ++++++++--- src/teachers-module/pages/EditClass.tsx | 37 ++++++---- src/teachers-module/pages/EditSchool.tsx | 20 +++--- src/teachers-module/pages/HomePage.tsx | 3 +- src/teachers-module/pages/LessonDetails.tsx | 61 ++++++++++------ src/teachers-module/pages/ManageClass.tsx | 15 +++- src/teachers-module/pages/ManageSchools.tsx | 8 ++- src/teachers-module/pages/ReqEditSchool.tsx | 19 ++--- src/teachers-module/pages/SchoolUsers.tsx | 28 +++++--- src/teachers-module/pages/SearchLessons.tsx | 8 ++- src/teachers-module/pages/SearchSchool.tsx | 11 ++- src/teachers-module/pages/ShowChapters.tsx | 25 ++++--- .../pages/ShowStudentsInAssignmentPage.tsx | 6 +- src/teachers-module/pages/StudentProfile.tsx | 18 +++-- src/teachers-module/pages/StudentReport.tsx | 32 ++++++--- .../pages/SubjectSelection.tsx | 29 +++++--- .../pages/TeacherLibraryAssignmentsLogic.ts | 16 +++-- src/teachers-module/pages/TeacherProfile.tsx | 6 +- src/teachers-module/pages/UserProfile.tsx | 3 +- src/utility/util.ts | 32 ++++++--- 61 files changed, 823 insertions(+), 404 deletions(-) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index ff01c4cef1..336deab893 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -68,6 +68,9 @@ import KidsAppLocation from '../teachers-module/pages/KidsAppLocation'; const AppRoutes = () => ( + + + diff --git a/src/common/constants.ts b/src/common/constants.ts index a4af7e5db3..6bae111b8c 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -555,6 +555,7 @@ export enum PAGES { COLORING_BOARD = '/coloring-board', STICKER_BOOK = '/sticker-book', STREAK_PAGE = '/streak-page', + ROOT = '/#', } export const enum ASSIGNMENT_TYPE { @@ -1077,6 +1078,7 @@ export const CURRENT_AVATAR_SUGGESTION_NO = 'currentAvatarSuggestion'; export const SHOW_DAILY_PROGRESS_FLAG = 'showAvatarDailyProgress'; export const CURRENT_SQLITE_VERSION = 'currentSqliteVersion'; +export const BUNDLED_IMPORT_APP_VERSION_KEY = 'bundledImportAppVersion'; export const CAMPAIGN_SEQUENCE_FINISHED = 'CAMPAIGN_SEQUENCE_FINISHED'; export const LIDO_COMMON_AUDIO_DIR = 'Lido-CommonAudios'; export const LIDO_COMMON_AUDIO_LANG_KEY = 'lido_common_audio_language'; diff --git a/src/components/LessonCard.tsx b/src/components/LessonCard.tsx index 9652d054ea..33a925cfdb 100644 --- a/src/components/LessonCard.tsx +++ b/src/components/LessonCard.tsx @@ -24,6 +24,8 @@ import DownloadLesson from './DownloadChapterAndLesson'; import { useOnlineOfflineErrorMessageHandler } from '../common/onlineOfflineErrorMessageHandler'; import logger from '../utility/logger'; +import { parsePath } from 'history'; + const LessonCard: React.FC<{ width: string; height: string; @@ -170,23 +172,28 @@ const LessonCard: React.FC<{ } if (assignment) { - history.push( - PAGES.LIVE_QUIZ_JOIN + `?assignmentId=${assignment?.id}`, - { + history.push({ + ...parsePath( + PAGES.LIVE_QUIZ_JOIN + `?assignmentId=${assignment?.id}`, + ), + state: { assignment: JSON.stringify(assignment), source: source, }, - ); + }); } else { - history.push( - PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, - { + history.push({ + ...parsePath( + PAGES.LIVE_QUIZ_GAME + + `?lessonId=${lesson.cocos_lesson_id}`, + ), + state: { courseId: resolvedCourse?.id, lesson: JSON.stringify(lesson), from: history.location.pathname + `?${CONTINUE}=true`, source: source, }, - ); + }); } } else { const playableLessonId = Util.getLessonBundleId(lesson); @@ -194,15 +201,18 @@ const LessonCard: React.FC<{ return; } const parmas = `?courseid=${lesson.cocos_subject_code}&chapterid=${lesson.cocos_chapter_code}&lessonid=${playableLessonId}`; - history.push(PAGES.LIDO_PLAYER + parmas, { - lessonId: playableLessonId, - courseDocId: resolvedCourse?.id, - course: JSON.stringify(resolvedCourse), - lesson: JSON.stringify(lesson), - assignment: assignment, - chapter: JSON.stringify(chapter), - from: history.location.pathname + `?${CONTINUE}=true`, - source: source, + history.push({ + ...parsePath(PAGES.LIDO_PLAYER + parmas), + state: { + lessonId: playableLessonId, + courseDocId: resolvedCourse?.id, + course: JSON.stringify(resolvedCourse), + lesson: JSON.stringify(lesson), + assignment: assignment, + chapter: JSON.stringify(chapter), + from: history.location.pathname + `?${CONTINUE}=true`, + source: source, + }, }); } } diff --git a/src/components/ProfileMenu/ProfileMenu.tsx b/src/components/ProfileMenu/ProfileMenu.tsx index c209da4a59..aad7bcd66e 100644 --- a/src/components/ProfileMenu/ProfileMenu.tsx +++ b/src/components/ProfileMenu/ProfileMenu.tsx @@ -30,6 +30,7 @@ import { schoolUtil } from '../../utility/schoolUtil'; import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; type ProfileMenuProps = { onClose: () => void; @@ -116,11 +117,21 @@ const ProfileMenu = ({ onClose }: ProfileMenuProps) => { } }; const onEdit = () => { - history.replace(PAGES.EDIT_STUDENT, { from: history.location.pathname }); + history.replace({ + ...parsePath(PAGES.EDIT_STUDENT), + state: { + from: history.location.pathname, + }, + }); }; const onLeaderboard = () => { - history.push(PAGES.LEADERBOARD, { from: history.location.pathname }); + history.push({ + ...parsePath(PAGES.LEADERBOARD), + state: { + from: history.location.pathname, + }, + }); }; const onStickerBook = async () => { @@ -139,7 +150,12 @@ const ProfileMenu = ({ onClose }: ProfileMenuProps) => { }); } } - history.push(PAGES.STICKER_BOOK, { from: history.location.pathname }); + history.push({ + ...parsePath(PAGES.STICKER_BOOK), + state: { + from: history.location.pathname, + }, + }); }; const onReward = () => { @@ -162,7 +178,12 @@ const ProfileMenu = ({ onClose }: ProfileMenuProps) => { school_ids: [], }); setGbUpdated(true); - history.replace(PAGES.DISPLAY_STUDENT, { from: history.location.pathname }); + history.replace({ + ...parsePath(PAGES.DISPLAY_STUDENT), + state: { + from: history.location.pathname, + }, + }); }; const onSchoolModeSwitchUser = async () => { @@ -175,9 +196,12 @@ const ProfileMenu = ({ onClose }: ProfileMenuProps) => { school_ids: [], }); setGbUpdated(true); - history.replace(PAGES.SELECT_MODE, { - from: history.location.pathname, - fromSchoolModeSwitchProfile: true, + history.replace({ + ...parsePath(PAGES.SELECT_MODE), + state: { + from: history.location.pathname, + fromSchoolModeSwitchProfile: true, + }, }); }; diff --git a/src/components/activationLesson/ActivationLessonBanner.tsx b/src/components/activationLesson/ActivationLessonBanner.tsx index 1abe220378..15ee0701a6 100644 --- a/src/components/activationLesson/ActivationLessonBanner.tsx +++ b/src/components/activationLesson/ActivationLessonBanner.tsx @@ -7,6 +7,7 @@ import { AudioUtil } from '../../utility/AudioUtil'; import logger from '../../utility/logger'; import { Util } from '../../utility/util'; import './ActivationLessonBanner.css'; +import { parsePath } from 'history'; const ENTRY_SOUND_EFFECT = '/assets/audios/common/generic_sound_effect.mp3'; const ACTIVATION_COUNTDOWN_SECONDS = 5; @@ -121,14 +122,17 @@ const ActivationLessonBanner: React.FC = ({ const params = `?courseid=${randomLesson.cocos_subject_code}&chapterid=${randomLesson.cocos_chapter_code}&lessonid=${playableLessonId}`; launchTimeoutRef.current = window.setTimeout(() => { - history.push(PAGES.LIDO_PLAYER + params, { - lessonId: playableLessonId, - courseDocId: CHIMPLE_DIGITAL_SKILLS, - lesson: JSON.stringify(randomLesson), - reward: true, - isDefaultLesson: true, - source, - from: history.location.pathname + history.location.search, + history.push({ + ...parsePath(PAGES.LIDO_PLAYER + params), + state: { + lessonId: playableLessonId, + courseDocId: CHIMPLE_DIGITAL_SKILLS, + lesson: JSON.stringify(randomLesson), + reward: true, + isDefaultLesson: true, + source, + from: history.location.pathname + history.location.search, + }, }); }, ACTIVATION_LAUNCH_DELAY_MS); } catch (error) { diff --git a/src/components/assignment/HomeworkPathwayStructure.tsx b/src/components/assignment/HomeworkPathwayStructure.tsx index 373fceb19c..69d48d1884 100644 --- a/src/components/assignment/HomeworkPathwayStructure.tsx +++ b/src/components/assignment/HomeworkPathwayStructure.tsx @@ -59,6 +59,7 @@ import { } from '../../utility/homeworkStickerFlow'; import { getCachedImageSrc } from '../../utility/imageCache'; import { mapInBatches } from '../../utility/batch'; +import { parsePath } from 'history'; interface HomeworkPathwayStructureProps { selectedSubject?: string | null; @@ -1427,9 +1428,12 @@ const HomeworkPathwayStructure: React.FC = ({ const shouldMarkRewardLesson = isRewardFeatureOn && hasTodayRewardRef.current; if (lesson.plugin_type === LIVE_QUIZ) { - history.push( - PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, - { + history.push({ + ...parsePath( + PAGES.LIVE_QUIZ_GAME + + `?lessonId=${lesson.cocos_lesson_id}`, + ), + state: { courseId: fetchedCourse?.id, lesson: JSON.stringify(lesson), from: history.location.pathname + `?${CONTINUE}=true`, @@ -1438,24 +1442,27 @@ const HomeworkPathwayStructure: React.FC = ({ reward: shouldMarkRewardLesson, source: SOURCE.LEARNING_PATHWAY_HOMEWORK, }, - ); + }); } else { const playableLessonId = Util.getLessonBundleId(lesson); if (!playableLessonId) { return; } const params = `?courseid=${lesson.cocos_subject_code}&chapterid=${lesson.cocos_chapter_code}&lessonid=${playableLessonId}`; - history.push(PAGES.LIDO_PLAYER + params, { - lessonId: playableLessonId, - courseDocId: fetchedCourse?.id, - course: JSON.stringify(fetchedCourse), - lesson: JSON.stringify(lesson), - chapter: JSON.stringify(fetchedChapter), - from: history.location.pathname + `?${CONTINUE}=true`, - isHomework: true, - homeworkIndex: lessonIdx, - reward: shouldMarkRewardLesson, - source: SOURCE.LEARNING_PATHWAY_HOMEWORK, + history.push({ + ...parsePath(PAGES.LIDO_PLAYER + params), + state: { + lessonId: playableLessonId, + courseDocId: fetchedCourse?.id, + course: JSON.stringify(fetchedCourse), + lesson: JSON.stringify(lesson), + chapter: JSON.stringify(fetchedChapter), + from: history.location.pathname + `?${CONTINUE}=true`, + isHomework: true, + homeworkIndex: lessonIdx, + reward: shouldMarkRewardLesson, + source: SOURCE.LEARNING_PATHWAY_HOMEWORK, + }, }); } }); @@ -2401,9 +2408,11 @@ const HomeworkPathwayStructure: React.FC = ({ : await api.getChapterById(chapterDocId).catch(() => currentChapter); if (lesson.plugin_type === LIVE_QUIZ) { - history.push( - PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, - { + history.push({ + ...parsePath( + PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, + ), + state: { courseId: courseDocId, lesson: JSON.stringify(lesson), from: history.location.pathname + `?${CONTINUE}=true`, @@ -2412,24 +2421,27 @@ const HomeworkPathwayStructure: React.FC = ({ reward: true, source: SOURCE.LEARNING_PATHWAY_HOMEWORK, }, - ); + }); } else { const playableLessonId = Util.getLessonBundleId(lesson); if (!playableLessonId) { return; } const params = `?courseid=${lesson.cocos_subject_code}&chapterid=${lesson.cocos_chapter_code}&lessonid=${playableLessonId}`; - history.push(PAGES.LIDO_PLAYER + params, { - lessonId: playableLessonId, - courseDocId, - course: JSON.stringify(nextCourse), - lesson: JSON.stringify(lesson), - chapter: JSON.stringify(nextChapter), - from: history.location.pathname + `?${CONTINUE}=true`, - isHomework: true, - homeworkIndex: homeworkPath.currentIndex, - reward: true, - source: SOURCE.LEARNING_PATHWAY_HOMEWORK, + history.push({ + ...parsePath(PAGES.LIDO_PLAYER + params), + state: { + lessonId: playableLessonId, + courseDocId, + course: JSON.stringify(nextCourse), + lesson: JSON.stringify(lesson), + chapter: JSON.stringify(nextChapter), + from: history.location.pathname + `?${CONTINUE}=true`, + isHomework: true, + homeworkIndex: homeworkPath.currentIndex, + reward: true, + source: SOURCE.LEARNING_PATHWAY_HOMEWORK, + }, }); } } catch (error) { diff --git a/src/components/learningPathway/StickerBookPreviewModal.logic.ts b/src/components/learningPathway/StickerBookPreviewModal.logic.ts index 68f8a439c1..ec59e5b2c8 100644 --- a/src/components/learningPathway/StickerBookPreviewModal.logic.ts +++ b/src/components/learningPathway/StickerBookPreviewModal.logic.ts @@ -20,6 +20,7 @@ import { getAppPathname } from '../../utility/routerLocation'; import { fetchStickerBookSvgText } from '../../utility/stickerBookAssets'; import { useStickerBookSave } from '../../hooks/useStickerBookSave'; import { resolveStickerBookSvgUrl } from '../../utility/stickerBookAssets'; +import { parsePath } from 'history'; export interface StickerBookModalData { source: 'learning_pathway' | 'homework_pathway'; @@ -774,12 +775,15 @@ export const useStickerBookPreviewModalLogic = ({ onClose(STICKER_BOOK_PREVIEW_ACKNOWLEDGE_CLOSE_REASON); } - history.push(PAGES.COLORING_BOARD, { - stickerBookId: data.stickerBookId, - svgRaw, - svgUrl: resolveStickerBookSvgUrl(data.stickerBookSvgUrl), - artworkTitle: data.stickerBookTitle || t('Sticker Book'), - returnTo: getAppPathname(), + history.push({ + ...parsePath(PAGES.COLORING_BOARD), + state: { + stickerBookId: data.stickerBookId, + svgRaw, + svgUrl: resolveStickerBookSvgUrl(data.stickerBookSvgUrl), + artworkTitle: data.stickerBookTitle || t('Sticker Book'), + returnTo: getAppPathname(), + }, }); }; diff --git a/src/components/parent/DataClear.tsx b/src/components/parent/DataClear.tsx index 9440727c7d..3c7a1ad6b2 100644 --- a/src/components/parent/DataClear.tsx +++ b/src/components/parent/DataClear.tsx @@ -5,6 +5,8 @@ import { SCHOOL_LOGIN, CURRENT_STUDENT, CACHE_TABLES_TO_CLEAR, + CURRENT_SQLITE_VERSION, + BUNDLED_IMPORT_APP_VERSION_KEY, } from '../../common/constants'; import { ServiceConfig } from '../../services/ServiceConfig'; @@ -16,6 +18,11 @@ const KEYS_TO_CLEAR = [ CURRENT_STUDENT, ] as const; +export const KEYS_TO_PRESERVE = new Set([ + BUNDLED_IMPORT_APP_VERSION_KEY, + CURRENT_SQLITE_VERSION, +]); + export async function clearCacheStorageJS() { if (!('caches' in globalThis)) return; try { @@ -27,7 +34,12 @@ export async function clearCacheStorageJS() { export async function clearLocalAndSession() { try { for (const k of KEYS_TO_CLEAR) localStorage.removeItem(k); - localStorage.clear?.(); + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key && !KEYS_TO_PRESERVE.has(key)) { + localStorage.removeItem(key); + } + } } catch {} try { sessionStorage.clear?.(); diff --git a/src/components/parent/ProfileCard.tsx b/src/components/parent/ProfileCard.tsx index 3b84205596..0b5e35a7fc 100644 --- a/src/components/parent/ProfileCard.tsx +++ b/src/components/parent/ProfileCard.tsx @@ -18,6 +18,7 @@ import { Util } from '../../utility/util'; import Loading from '../Loading'; import DialogBoxButtons from './DialogBoxButtons'; import './ProfileCard.css'; +import { parsePath } from 'history'; const EDIT_PROFILE_ICON_SRC = '/assets/edit-profile-icon.svg'; const EDIT_PROFILE_DIALOG_ICON_SRC = @@ -196,9 +197,12 @@ const ProfileCard: React.FC<{ return; } void Util.logEvent(EVENTS.PROFILE_CREATION_CLICKED, {}); - history.push(PAGES.CREATE_STUDENT, { - isEdit: false, - from: `${history.location.pathname}${history.location.search}`, + history.push({ + ...parsePath(PAGES.CREATE_STUDENT), + state: { + isEdit: false, + from: `${history.location.pathname}${history.location.search}`, + }, }); }} > @@ -227,8 +231,11 @@ const ProfileCard: React.FC<{ logProfileCardAction('edit_profile'); // Passing false to not change the student language as it is not required for edit student screen await Util.setCurrentStudent(user, undefined, false, false); - history.replace(PAGES.EDIT_STUDENT, { - from: history.location.pathname, + history.replace({ + ...parsePath(PAGES.EDIT_STUDENT), + state: { + from: history.location.pathname, + }, }); setShowDialogBox(false); }} diff --git a/src/hooks/usePathwayData.ts b/src/hooks/usePathwayData.ts index d123757a8c..263ae30c87 100644 --- a/src/hooks/usePathwayData.ts +++ b/src/hooks/usePathwayData.ts @@ -29,6 +29,7 @@ import { schoolUtil } from '../utility/schoolUtil'; import { LessonNode } from './useLearningPath'; import logger from '../utility/logger'; import { AudioUtil } from '../utility/AudioUtil'; +import { parsePath } from 'history'; export interface MascotProps { stateMachine: string; @@ -382,9 +383,11 @@ export const usePathwayData = () => { if (!lesson) return; if (lesson.plugin_type === LIVE_QUIZ) { - history.replace( - PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, - { + history.replace({ + ...parsePath( + PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, + ), + state: { courseId: course.course_id, lesson: JSON.stringify(lesson), from: history.location.pathname + `?${CONTINUE}=true`, @@ -394,26 +397,29 @@ export const usePathwayData = () => { is_assessment: isAssessment, source: pathItem?.source ?? SOURCE.LEARNING_PATHWAY_HOME_NO_PAL, }, - ); + }); } else { const playableLessonId = Util.getLessonBundleId(lesson); if (!playableLessonId) { return; } const params = `?courseid=${lesson.cocos_subject_code}&chapterid=${lesson.cocos_chapter_code}&lessonid=${playableLessonId}`; - history.replace(PAGES.LIDO_PLAYER + params, { - lessonId: playableLessonId, - courseDocId: course.course_id, - course: JSON.stringify(currentCourse), - lesson: JSON.stringify(lesson), - chapter: JSON.stringify(currentChapter), - from: history.location.pathname + `?${CONTINUE}=true`, - learning_path: true, - reward: true, - skillId: pathItem?.skill_id, - is_assessment: isAssessment, - assessmentId, - source: pathItem?.source ?? SOURCE.LEARNING_PATHWAY_HOME_NO_PAL, + history.replace({ + ...parsePath(PAGES.LIDO_PLAYER + params), + state: { + lessonId: playableLessonId, + courseDocId: course.course_id, + course: JSON.stringify(currentCourse), + lesson: JSON.stringify(lesson), + chapter: JSON.stringify(currentChapter), + from: history.location.pathname + `?${CONTINUE}=true`, + learning_path: true, + reward: true, + skillId: pathItem?.skill_id, + is_assessment: isAssessment, + assessmentId, + source: pathItem?.source ?? SOURCE.LEARNING_PATHWAY_HOME_NO_PAL, + }, }); } } catch (error) { diff --git a/src/hooks/usePathwaySVG.ts b/src/hooks/usePathwaySVG.ts index 71c2d2554a..aeb812a1b3 100644 --- a/src/hooks/usePathwaySVG.ts +++ b/src/hooks/usePathwaySVG.ts @@ -42,6 +42,7 @@ import { t } from 'i18next'; import { getCachedImageSrc } from '../utility/imageCache'; import { mapInBatches } from '../utility/batch'; import { schoolUtil } from '../utility/schoolUtil'; +import { parsePath } from 'history'; interface UsePathwaySVGParams { containerRef: RefObject; @@ -1917,9 +1918,11 @@ export function usePathwaySVG({ const currentChapter = (window as any).__currentChapterForPathway__; if (lesson.plugin_type === LIVE_QUIZ) { - history.replace( - PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, - { + history.replace({ + ...parsePath( + PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, + ), + state: { courseId: course.course_id, lesson: JSON.stringify(lesson), from: history.location.pathname + `?${CONTINUE}=true`, @@ -1928,7 +1931,7 @@ export function usePathwaySVG({ is_assessment: is_assessment, source: source, }, - ); + }); return; } @@ -1938,18 +1941,21 @@ export function usePathwaySVG({ } const p = `?courseid=${lesson.cocos_subject_code}&chapterid=${lesson.cocos_chapter_code}&lessonid=${playableLessonId}`; - history.replace(PAGES.LIDO_PLAYER + p, { - lessonId: playableLessonId, - courseDocId: course.course_id, - course: JSON.stringify(currentCourse), - lesson: JSON.stringify(lesson), - chapter: JSON.stringify(currentChapter), - from: history.location.pathname + `?${CONTINUE}=true`, - learning_path: true, - skillId: skillId, - is_assessment: is_assessment, - assessmentId: assessmentId, - source: source, + history.replace({ + ...parsePath(PAGES.LIDO_PLAYER + p), + state: { + lessonId: playableLessonId, + courseDocId: course.course_id, + course: JSON.stringify(currentCourse), + lesson: JSON.stringify(lesson), + chapter: JSON.stringify(currentChapter), + from: history.location.pathname + `?${CONTINUE}=true`, + learning_path: true, + skillId: skillId, + is_assessment: is_assessment, + assessmentId: assessmentId, + source: source, + }, }); } diff --git a/src/ops-console/components/SchoolDetailsComponents/SchoolOverview.tsx b/src/ops-console/components/SchoolDetailsComponents/SchoolOverview.tsx index 67790255f8..c65722f1de 100644 --- a/src/ops-console/components/SchoolDetailsComponents/SchoolOverview.tsx +++ b/src/ops-console/components/SchoolDetailsComponents/SchoolOverview.tsx @@ -14,6 +14,7 @@ import SubjectCurriculumCard from '../SubjectCurriculumCard'; import { useAppSelector } from '../../../redux/hooks'; import { RootState } from '../../../redux/store'; import { AuthState } from '../../../redux/slices/auth/authSlice'; +import { parsePath } from 'history'; interface SchoolOverviewProps { data: any; @@ -137,10 +138,12 @@ const SchoolOverview: React.FC = ({ data, isMobile }) => { className="schooloverview-view-all-interactions-btn" sx={{ textTransform: 'none' }} onClick={() => - history.replace( - `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ACTIVITIES_PAGE}`, - data.schoolData, - ) + history.replace({ + ...parsePath( + `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ACTIVITIES_PAGE}`, + ), + state: data.schoolData, + }) } > {t('View All Interactions')} @@ -174,10 +177,12 @@ const SchoolOverview: React.FC = ({ data, isMobile }) => { variant="outlined" sx={{ mt: 2, textTransform: 'none' }} onClick={() => - history.replace( - `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ACTIVITIES_PAGE}`, - data.schoolData, - ) + history.replace({ + ...parsePath( + `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ACTIVITIES_PAGE}`, + ), + state: data.schoolData, + }) } > {t('View All Interactions')} @@ -258,10 +263,12 @@ const SchoolOverview: React.FC = ({ data, isMobile }) => { items={schoolDetailsItems} showEditIcon={isExternalUser ? false : haveAccess} onEditClick={() => - history.replace( - `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ADD_SCHOOL_PAGE}`, - data, - ) + history.replace({ + ...parsePath( + `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ADD_SCHOOL_PAGE}`, + ), + state: data, + }) } /> @@ -326,10 +333,12 @@ const SchoolOverview: React.FC = ({ data, isMobile }) => { items={schoolDetailsItems} showEditIcon={isExternalUser ? false : haveAccess} onEditClick={() => - history.replace( - `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ADD_SCHOOL_PAGE}`, - data, - ) + history.replace({ + ...parsePath( + `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ADD_SCHOOL_PAGE}`, + ), + state: data, + }) } /> diff --git a/src/ops-console/components/Sidebar.tsx b/src/ops-console/components/Sidebar.tsx index 39c0af7e20..9435656ad3 100644 --- a/src/ops-console/components/Sidebar.tsx +++ b/src/ops-console/components/Sidebar.tsx @@ -35,6 +35,8 @@ import { RootState } from '../../redux/store'; import { AuthState } from '../../redux/slices/auth/authSlice'; import { logAuthDebug } from '../../utility/authDebug'; import { getAppPathname } from '../../utility/routerLocation'; +import { KEYS_TO_PRESERVE } from '../../components/parent/DataClear'; +import { parsePath } from 'history'; interface SidebarProps { name: string; @@ -116,7 +118,10 @@ const Sidebar: React.FC = ({ name, email, photo }) => { const switchUserToTeacher = async () => { if (localSchool && localClass) { schoolUtil.setCurrMode(MODES.TEACHER); - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); } else if (schools && schools.length > 0) { if (schools.length === 1) { Util.setCurrentSchool(schools[0].school, schools[0].role); @@ -127,7 +132,10 @@ const Sidebar: React.FC = ({ name, email, photo }) => { if (tempClasses.length > 0) { Util.setCurrentClass(tempClasses[0]); schoolUtil.setCurrMode(MODES.TEACHER); - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); } } else { schoolUtil.setCurrMode(MODES.TEACHER); @@ -170,7 +178,12 @@ const Sidebar: React.FC = ({ name, email, photo }) => { }); await auth.logOut(); Util.unSubscribeToClassTopicForAllStudents(); - localStorage.clear(); + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key && !KEYS_TO_PRESERVE.has(key)) { + localStorage.removeItem(key); + } + } const serviceInstance = ServiceConfig.getInstance(APIMode.SQLITE); serviceInstance.switchMode(APIMode.SQLITE); logAuthDebug('Navigating to login after ops console logout.', { diff --git a/src/ops-console/pages/ActivitiesPage.tsx b/src/ops-console/pages/ActivitiesPage.tsx index d9224f7fe4..909b0b5b74 100644 --- a/src/ops-console/pages/ActivitiesPage.tsx +++ b/src/ops-console/pages/ActivitiesPage.tsx @@ -15,6 +15,7 @@ import './ActivitiesPage.css'; import SchoolNameHeaderComponent from '../components/SchoolDetailsComponents/SchoolNameHeaderComponent'; import { OpsUtil } from '../OpsUtility/OpsUtil'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const DEFAULT_PAGE_SIZE = 20; @@ -239,10 +240,12 @@ const ActivitiesPage: React.FC = () => { visitDetails: row.visitDetails || null, }; - history.push( - `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ACTIVITIES_PAGE}${PAGES.SCHOOL_ACTIVITIES}`, - data, - ); + history.push({ + ...parsePath( + `${PAGES.SIDEBAR_PAGE}${PAGES.SCHOOL_LIST}${PAGES.ACTIVITIES_PAGE}${PAGES.SCHOOL_ACTIVITIES}`, + ), + state: data, + }); }; const pageCount = Math.ceil(total / DEFAULT_PAGE_SIZE); diff --git a/src/pages/DisplayStudents.tsx b/src/pages/DisplayStudents.tsx index 7c72a3e54a..a088656eff 100644 --- a/src/pages/DisplayStudents.tsx +++ b/src/pages/DisplayStudents.tsx @@ -23,6 +23,8 @@ import { schoolUtil } from '../utility/schoolUtil'; import { Util } from '../utility/util'; import BrandLogoIcon from './assets/brandLogoIcon.svg?raw'; import './DisplayStudents.css'; +import { parsePath } from 'history'; + const DisplayStudents: FC<{}> = () => { const [isLoading, setIsLoading] = useState(true); const [students, setStudents] = useState[]>(); @@ -69,8 +71,11 @@ const DisplayStudents: FC<{}> = () => { storedMapStr, ); if (!mergedStudents || mergedStudents.length < 1) { - history.replace(PAGES.CREATE_STUDENT, { - showBackButton: false, + history.replace({ + ...parsePath(PAGES.CREATE_STUDENT), + state: { + showBackButton: false, + }, }); return; } @@ -105,8 +110,11 @@ const DisplayStudents: FC<{}> = () => { }); setGbUpdated(true); if (!student.language_id) { - history.replace(PAGES.EDIT_STUDENT, { - from: history.location.pathname, + history.replace({ + ...parsePath(PAGES.EDIT_STUDENT), + state: { + from: history.location.pathname, + }, }); } else { history.replace(PAGES.HOME + getAppSearch()); diff --git a/src/pages/LidoPlayer.tsx b/src/pages/LidoPlayer.tsx index 2d18af3177..8b900ada75 100644 --- a/src/pages/LidoPlayer.tsx +++ b/src/pages/LidoPlayer.tsx @@ -48,6 +48,7 @@ import { REMOTE_CONFIG_KEYS, } from '../services/RemoteConfig'; import { getAppPathname, getAppSearchParams } from '../utility/routerLocation'; +import { parsePath } from 'history'; const HOMEWORK_REWARD_COMPLETED_INDEX_KEY = 'homework_reward_completed_index'; const PENDING_HOMEWORK_REWARD_TRANSITION_KEY = @@ -534,7 +535,7 @@ const LidoPlayer: FC = () => { targetPath = `${fromPath}${separator}isReload=true`; } - history.replace(targetPath, returnState); + history.replace({ ...parsePath(targetPath), state: returnState }); setIsLoading(false); setTimeout(() => { isExitingRef.current = false; diff --git a/src/pages/LiveQuizGame.tsx b/src/pages/LiveQuizGame.tsx index 1250c37ec5..79c1be8231 100644 --- a/src/pages/LiveQuizGame.tsx +++ b/src/pages/LiveQuizGame.tsx @@ -22,6 +22,7 @@ import { Capacitor } from '@capacitor/core'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; import { useGrowthBook } from '@growthbook/growthbook-react'; import { getAppSearchParams } from '../utility/routerLocation'; +import { parsePath } from 'history'; const LiveQuizGame: FC = () => { const api = ServiceConfig.getI().apiHandler; @@ -139,16 +140,25 @@ const LiveQuizGame: FC = () => { fromLiveQuiz: true, }; if (Capacitor.isNativePlatform()) { - history.replace(fromPath + '&isReload=false', returnState); + history.replace({ + ...parsePath(fromPath + '&isReload=false'), + state: returnState, + }); } else { if (!!urlParams.get('isReload')) { if (fromPath.includes('?')) { - history.replace(fromPath + '&isReload=true', returnState); + history.replace({ + ...parsePath(fromPath + '&isReload=true'), + state: returnState, + }); } else { - history.replace(fromPath + '?isReload=true', returnState); + history.replace({ + ...parsePath(fromPath + '?isReload=true'), + state: returnState, + }); } } else { - history.replace(fromPath, returnState); + history.replace({ ...parsePath(fromPath), state: returnState }); } } }; diff --git a/src/pages/LiveQuizRoom.tsx b/src/pages/LiveQuizRoom.tsx index 323dcbb507..893958fd80 100644 --- a/src/pages/LiveQuizRoom.tsx +++ b/src/pages/LiveQuizRoom.tsx @@ -13,6 +13,8 @@ import BackButton from '../components/common/BackButton'; import SkeltonLoading from '../components/SkeltonLoading'; import { ServiceConfig } from '../services/ServiceConfig'; import { getAppSearchParams } from '../utility/routerLocation'; +import { parsePath } from 'history'; + const LiveQuizRoom: React.FC = () => { const [students, setStudents] = useState( new Map>(), @@ -159,7 +161,10 @@ const LiveQuizRoom: React.FC = () => { } else { const gamePath = PAGES.LIVE_QUIZ_GAME + '?liveRoomId=' + res; if (state.source) { - history.replace(gamePath, { source: state.source }); + history.replace({ + ...parsePath(gamePath), + state: { source: state.source }, + }); } else { history.replace(gamePath); } diff --git a/src/pages/StickerBook.tsx b/src/pages/StickerBook.tsx index e05e8c5c3d..c7e486fb2e 100644 --- a/src/pages/StickerBook.tsx +++ b/src/pages/StickerBook.tsx @@ -25,6 +25,7 @@ import { resolveStickerBookSvgUrl, } from '../utility/stickerBookAssets'; import { useStickerBookSave } from '../hooks/useStickerBookSave'; +import { parsePath } from 'history'; type CurrentProgress = { bookId: string; @@ -251,12 +252,15 @@ const StickerBook: React.FC = () => { const onPaint = () => { if (!selectedBook) return; const svgUrl = resolveStickerBookSvgUrl(selectedBook.svg_url ?? ''); - history.push(PAGES.COLORING_BOARD, { - stickerBookId: selectedBook.id, - svgRaw: svgRaw ?? undefined, - svgUrl, - artworkTitle: selectedBook.title ?? t('Sticker Book'), - returnTo: PAGES.STICKER_BOOK, + history.push({ + ...parsePath(PAGES.COLORING_BOARD), + state: { + stickerBookId: selectedBook.id, + svgRaw: svgRaw ?? undefined, + svgUrl, + artworkTitle: selectedBook.title ?? t('Sticker Book'), + returnTo: PAGES.STICKER_BOOK, + }, }); }; diff --git a/src/pages/TermsAndConditions.tsx b/src/pages/TermsAndConditions.tsx index 07cbe440e5..223650b901 100644 --- a/src/pages/TermsAndConditions.tsx +++ b/src/pages/TermsAndConditions.tsx @@ -14,6 +14,7 @@ import { resolveTermsBaseUrl, } from '../utility/termsAndConditions'; import './TermsAndConditions.css'; +import { parsePath } from 'history'; type TermsPageLocationState = { from?: string; @@ -82,10 +83,12 @@ const TermsAndConditions: React.FC = () => { const handleClose = () => { if (returnLocation?.pathname) { - history.replace( - `${returnLocation.pathname}${returnLocation.search ?? ''}${returnLocation.hash ?? ''}`, - returnLocation.state, - ); + history.replace({ + ...parsePath( + `${returnLocation.pathname}${returnLocation.search ?? ''}${returnLocation.hash ?? ''}`, + ), + state: returnLocation.state, + }); return; } diff --git a/src/services/auth/SupabaseAuth.ts b/src/services/auth/SupabaseAuth.ts index 5917e5bd2f..ac1b43ea98 100644 --- a/src/services/auth/SupabaseAuth.ts +++ b/src/services/auth/SupabaseAuth.ts @@ -273,7 +273,7 @@ export class SupabaseAuth implements ServiceAuth { }, }); } else { - const redirectTo = window.location.origin; + const redirectTo = `${window.location.origin}${BASE_NAME}`; markWebGoogleLoginPending(); const { error } = await this._auth.signInWithOAuth({ provider: 'google', diff --git a/src/teachers-module/components/ChapterWiseLessons.tsx b/src/teachers-module/components/ChapterWiseLessons.tsx index 2ea0cd3a4a..90c497dd21 100644 --- a/src/teachers-module/components/ChapterWiseLessons.tsx +++ b/src/teachers-module/components/ChapterWiseLessons.tsx @@ -7,6 +7,7 @@ import SelectIconImage from '../../components/displaySubjects/SelectIconImage'; import SelectIcon from './SelectIcon'; import AssignedBadgeIcon from './AssignedBadgeIcon'; import './ChapterWiseLessons.css'; +import { parsePath } from 'history'; type ChapterGroup = { chapterId: string; @@ -60,19 +61,22 @@ const ChapterWiseLessons: React.FC = ({ gradeName: string, course?: TableTypes<'course'>, ) => { - history.replace(PAGES.LESSON_DETAILS, { - course: course ?? null, - lesson, - chapterId, - chapterName, - gradeName, - subjectName: - course?.code?.toUpperCase() || - course?.name || - lesson.cocos_subject_code || - '', - from: PAGES.SEARCH_LESSON, - selectedLesson, + history.replace({ + ...parsePath(PAGES.LESSON_DETAILS), + state: { + course: course ?? null, + lesson, + chapterId, + chapterName, + gradeName, + subjectName: + course?.code?.toUpperCase() || + course?.name || + lesson.cocos_subject_code || + '', + from: PAGES.SEARCH_LESSON, + selectedLesson, + }, }); }; diff --git a/src/teachers-module/components/CreateSchoolPrompt.tsx b/src/teachers-module/components/CreateSchoolPrompt.tsx index c9daaeb271..5ffd9815ab 100644 --- a/src/teachers-module/components/CreateSchoolPrompt.tsx +++ b/src/teachers-module/components/CreateSchoolPrompt.tsx @@ -4,6 +4,7 @@ import { useHistory } from 'react-router'; import { PAGES } from '../../common/constants'; import './CreateSchoolPrompt.css'; import { t } from 'i18next'; +import { parsePath } from 'history'; interface CreateSchoolPromptProps { country?: string; @@ -21,12 +22,15 @@ const CreateSchoolPrompt: FC = ({ const history = useHistory(); const handleCreateSchool = () => { - history.push(PAGES.CREATE_SCHOOL, { - origin: PAGES.SEARCH_SCHOOL, - country, - state, - district, - block, + history.push({ + ...parsePath(PAGES.CREATE_SCHOOL), + state: { + origin: PAGES.SEARCH_SCHOOL, + country, + state, + district, + block, + }, }); }; diff --git a/src/teachers-module/components/homePage/assignment/AssignScreen.tsx b/src/teachers-module/components/homePage/assignment/AssignScreen.tsx index a7bcf536cd..311b42caa0 100644 --- a/src/teachers-module/components/homePage/assignment/AssignScreen.tsx +++ b/src/teachers-module/components/homePage/assignment/AssignScreen.tsx @@ -12,6 +12,7 @@ import { PAGES } from '../../../../common/constants'; import { ServiceConfig } from '../../../../services/ServiceConfig'; import { Toast } from '@capacitor/toast'; import logger from '../../../../utility/logger'; +import { parsePath } from 'history'; interface AssignScreenProps { onLibraryClick: () => void; @@ -61,10 +62,13 @@ const AssignScreen: FC = ({ return; } // ✅ Success → Navigate - history.replace(PAGES.QR_ASSIGNMENTS, { - chapterId: response.chapter_id, - courseId: response.course_id, - fromPage: PAGES.HOME_PAGE, + history.replace({ + ...parsePath(PAGES.QR_ASSIGNMENTS), + state: { + chapterId: response.chapter_id, + courseId: response.course_id, + fromPage: PAGES.HOME_PAGE, + }, }); } catch (error) { logger.error('Scan failed:', error); diff --git a/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx b/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx index f11bf13f21..5e6ed409d2 100644 --- a/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx +++ b/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx @@ -30,6 +30,7 @@ import { getStreakTargetRect, triggerStreakRewardPulse, } from '../../../../common/streakRewardBridge'; +import { parsePath } from 'history'; interface LessonDetail { subject: string; @@ -916,11 +917,17 @@ const CreateSelectedAssignment = ({ leftButtonText={t('Cancel') ?? ''} leftButtonHandler={() => { setShowConfirm(false); - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 2 }, + }); }} onDidDismiss={() => { setShowConfirm(false); - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 2 }, + }); }} rightButtonText={t('Share') ?? ''} rightButtonHandler={async () => { @@ -930,7 +937,10 @@ const CreateSelectedAssignment = ({ text, 'Assignment Assigned', ); - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 2 }, + }); }} > diff --git a/src/teachers-module/components/homePage/assignment/QRAssignments.tsx b/src/teachers-module/components/homePage/assignment/QRAssignments.tsx index 992c8fd996..8c77127338 100644 --- a/src/teachers-module/components/homePage/assignment/QRAssignments.tsx +++ b/src/teachers-module/components/homePage/assignment/QRAssignments.tsx @@ -13,6 +13,7 @@ import AssigmentCount from '../../library/AssignmentCount'; import logger from '../../../../utility/logger'; import AssignedVisibilityToggle from '../../AssignedVisibilityToggle'; import AssignedBadgeIcon from '../../AssignedBadgeIcon'; +import { parsePath } from 'history'; type LessonUI = { id: string; @@ -129,7 +130,10 @@ const QRAssignments: React.FC = () => { isBackButton={true} onBackButtonClick={() => { if (location.state?.fromPage === PAGES.HOME_PAGE) { - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 2 }, + }); return; } history.goBack(); @@ -273,24 +277,27 @@ const QRAssignments: React.FC = () => { }, }; - history.push(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, { - fromPage: PAGES.QR_ASSIGNMENTS, - selectedAssignments, - manualAssignments: { - [location.state.courseId]: { - lessons: lessons - .filter((l) => selectedLessonIds.includes(l.id)) - .map((l) => ({ - ...l, - source: AssignmentSource.QR_CODE, - })), + history.push({ + ...parsePath(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE), + state: { + fromPage: PAGES.QR_ASSIGNMENTS, + selectedAssignments, + manualAssignments: { + [location.state.courseId]: { + lessons: lessons + .filter((l) => selectedLessonIds.includes(l.id)) + .map((l) => ({ + ...l, + source: AssignmentSource.QR_CODE, + })), + }, + }, + recommendedAssignments: {}, + qrAssignmentNavigationState: { + chapterId: location.state.chapterId, + courseId: location.state.courseId, + fromPage: location.state.fromPage, }, - }, - recommendedAssignments: {}, - qrAssignmentNavigationState: { - chapterId: location.state.chapterId, - courseId: location.state.courseId, - fromPage: location.state.fromPage, }, }); }} diff --git a/src/teachers-module/components/homePage/assignment/ScanRedirect.tsx b/src/teachers-module/components/homePage/assignment/ScanRedirect.tsx index e71195e8e5..f9cb956ef5 100644 --- a/src/teachers-module/components/homePage/assignment/ScanRedirect.tsx +++ b/src/teachers-module/components/homePage/assignment/ScanRedirect.tsx @@ -1,12 +1,13 @@ import { FC, useEffect } from 'react'; import { useHistory } from 'react-router'; import { PAGES } from '../../../../common/constants'; +import { parsePath } from 'history'; const ScanRedirect: FC = () => { const history = useHistory(); useEffect(() => { // Immediately navigate to TeacherAssignment page - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ ...parsePath(PAGES.HOME_PAGE), state: { tabValue: 2 } }); }, [history]); return null; diff --git a/src/teachers-module/components/homePage/assignment/TeacherAssignment.tsx b/src/teachers-module/components/homePage/assignment/TeacherAssignment.tsx index 19eb607d34..72fb90abbd 100644 --- a/src/teachers-module/components/homePage/assignment/TeacherAssignment.tsx +++ b/src/teachers-module/components/homePage/assignment/TeacherAssignment.tsx @@ -22,6 +22,7 @@ import Loading from '../../../../components/Loading'; import { checkmarkCircle, ellipseOutline } from 'ionicons/icons'; import { IonIcon } from '@ionic/react'; import logger from '../../../../utility/logger'; +import { parsePath } from 'history'; declare global { interface Window { @@ -636,9 +637,12 @@ const TeacherAssignment: FC<{ finalLessonsJson, ); - history.push(PAGES.QR_ASSIGNMENTS, { - chapterId: result.chapter_id, - courseId: result.course_id, + history.push({ + ...parsePath(PAGES.QR_ASSIGNMENTS), + state: { + chapterId: result.chapter_id, + courseId: result.course_id, + }, }); // await init(); } catch (error) { @@ -861,10 +865,13 @@ const TeacherAssignment: FC<{ .count > 0 ) { - history.replace(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, { - selectedAssignments: selectedLessonsCount, - manualAssignments: manualAssignments, - recommendedAssignments: recommendedAssignments, + history.replace({ + ...parsePath(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE), + state: { + selectedAssignments: selectedLessonsCount, + manualAssignments: manualAssignments, + recommendedAssignments: recommendedAssignments, + }, }); } else { await Toast.show({ diff --git a/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.tsx b/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.tsx index a782ddb64c..fb8518bf0a 100644 --- a/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.tsx +++ b/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.tsx @@ -16,6 +16,7 @@ import { } from './AssignmentUtil'; import Loading from '../../../../components/Loading'; import logger from '../../../../utility/logger'; +import { parsePath } from 'history'; export enum TeacherRecommendedAssignmentsType { RECOMMENDED = 'recommended', @@ -110,7 +111,7 @@ const TeacherRecommendedAssignments: FC = () => { }; const handleRecommendedBack = () => { - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ ...parsePath(PAGES.HOME_PAGE), state: { tabValue: 2 } }); }; return (
{ buildRecommendedPayload(recommendedAssignments); if (Object.keys(selectedAssignments).length > 0) { - history.replace(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, { - fromPage: PAGES.TEACHER_RECOMMENDED_ASSIGNMENTS, - selectedAssignments: { - recommended: formattedRecommended, + history.replace({ + ...parsePath(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE), + state: { + fromPage: PAGES.TEACHER_RECOMMENDED_ASSIGNMENTS, + selectedAssignments: { + recommended: formattedRecommended, + }, + manualAssignments: {}, + recommendedAssignments, }, - manualAssignments: {}, - recommendedAssignments, }); } }} diff --git a/src/teachers-module/components/homePage/dashBoard/DashBoard.tsx b/src/teachers-module/components/homePage/dashBoard/DashBoard.tsx index b5c8bd5aff..9edbab3169 100644 --- a/src/teachers-module/components/homePage/dashBoard/DashBoard.tsx +++ b/src/teachers-module/components/homePage/dashBoard/DashBoard.tsx @@ -17,6 +17,8 @@ import Loading from '../../../../components/Loading'; import { useHistory } from 'react-router'; import { Util } from '../../../../utility/util'; import { subDays } from 'date-fns'; +import { parsePath } from 'history'; + type SubjectOption = TableTypes<'course'> | typeof ALL_SUBJECT; const DashBoard: React.FC = ({}) => { @@ -85,12 +87,15 @@ const DashBoard: React.FC = ({}) => { const startDate = subDays(new Date(), 6); const endDate = new Date(); - history.replace(PAGES.STUDENT_REPORT, { - student, - startDate, - endDate, - classDoc: current_class, - fromDashboardBand: true, + history.push({ + ...parsePath(PAGES.STUDENT_REPORT), + state: { + student, + startDate, + endDate, + classDoc: current_class, + fromDashboardBand: true, + }, }); }; diff --git a/src/teachers-module/components/library/Library.tsx b/src/teachers-module/components/library/Library.tsx index d51441ee0d..2eae9980a8 100644 --- a/src/teachers-module/components/library/Library.tsx +++ b/src/teachers-module/components/library/Library.tsx @@ -6,6 +6,7 @@ import { PAGES, TableTypes } from '../../../common/constants'; import { ServiceConfig } from '../../../services/ServiceConfig'; import { Util } from '../../../utility/util'; import { t } from 'i18next'; +import { parsePath } from 'history'; const Library: React.FC = () => { const [courses, setCourses] = useState[]>([]); @@ -48,7 +49,13 @@ const Library: React.FC = () => { course={course} gradeName={gradeName} handleCourseCLick={() => { - history.replace(PAGES.SHOW_CHAPTERS, { course, gradeName }); + history.push({ + ...parsePath(PAGES.SHOW_CHAPTERS), + state: { + course, + gradeName, + }, + }); }} /> ); diff --git a/src/teachers-module/components/reports/ReportsTable.tsx b/src/teachers-module/components/reports/ReportsTable.tsx index 7d852c89ec..af1184dbd0 100644 --- a/src/teachers-module/components/reports/ReportsTable.tsx +++ b/src/teachers-module/components/reports/ReportsTable.tsx @@ -25,6 +25,7 @@ import { RoleType } from '../../../interface/modelInterfaces'; import { useAppSelector } from '../../../redux/hooks'; import { RootState } from '../../../redux/store'; import { AuthState } from '../../../redux/slices/auth/authSlice'; +import { parsePath } from 'history'; interface ReportTableProps { handleButtonClick?: (isOpen: boolean) => void; @@ -360,13 +361,16 @@ const ReportTable: React.FC = ({ if (selected) setSelectedChapter(selected); }; const handleViewClickDetails = (student: TableTypes<'user'>) => { - history.replace(PAGES.STUDENT_REPORT, { - student: student, - startDate: dateRange.startDate, - endDate: dateRange.endDate, - selectedType: selectedType, - isAssignments: isAssignments, - sortType: sortType, + history.push({ + ...parsePath(PAGES.STUDENT_REPORT), + state: { + student: student, + startDate: dateRange.startDate, + endDate: dateRange.endDate, + selectedType: selectedType, + isAssignments: isAssignments, + sortType: sortType, + }, }); }; diff --git a/src/teachers-module/components/schoolComponent/DetailList.tsx b/src/teachers-module/components/schoolComponent/DetailList.tsx index ec4e1caf0e..bd34e17baf 100644 --- a/src/teachers-module/components/schoolComponent/DetailList.tsx +++ b/src/teachers-module/components/schoolComponent/DetailList.tsx @@ -14,6 +14,7 @@ import { AuthState } from '../../../redux/slices/auth/authSlice'; import { RootState } from '../../../redux/store'; import { schoolUtil } from '../../../utility/schoolUtil'; import './DetailList.css'; +import { parsePath } from 'history'; interface DetailListProps { type: IconType; @@ -38,31 +39,46 @@ const DetailList: React.FC = ({ type, school, data }) => { const handleItemClick = (item: any) => { if (type === IconType.SCHOOL) { - history.replace(PAGES.SCHOOL_PROFILE, { - school: item.school, - role: item.role, + history.replace({ + ...parsePath(PAGES.SCHOOL_PROFILE), + state: { + school: item.school, + role: item.role, + }, }); } else { - history.replace(PAGES.CLASS_PROFILE, { school, classDoc: item }); + history.replace({ + ...parsePath(PAGES.CLASS_PROFILE), + state: { school, classDoc: item }, + }); } }; const handleUserIconClick = (item: any) => { if (type === IconType.SCHOOL) { - history.replace(PAGES.SCHOOL_USERS, { - school: item.school, - role: item.role, + history.replace({ + ...parsePath(PAGES.SCHOOL_USERS), + state: { + school: item.school, + role: item.role, + }, }); } else { - history.replace(PAGES.CLASS_USERS, item); + history.replace({ ...parsePath(PAGES.CLASS_USERS), state: item }); } }; const handleSubjectIconClick = (item: any) => { if (type === IconType.SCHOOL) { - history.replace(PAGES.SUBJECTS_PAGE, { schoolId: item.school.id }); + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { schoolId: item.school.id }, + }); } else { - history.replace(PAGES.SUBJECTS_PAGE, { classId: item.id }); + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { classId: item.id }, + }); } }; diff --git a/src/teachers-module/components/studentProfile/UserDetail.tsx b/src/teachers-module/components/studentProfile/UserDetail.tsx index f42e826f92..dfb6d7782d 100644 --- a/src/teachers-module/components/studentProfile/UserDetail.tsx +++ b/src/teachers-module/components/studentProfile/UserDetail.tsx @@ -8,6 +8,8 @@ import { TableTypes, } from '../../../common/constants'; import { useHistory } from 'react-router-dom'; +import { parsePath } from 'history'; + const UserDetail: React.FC<{ user: TableTypes<'user'>; classDoc: TableTypes<'class'>; @@ -17,16 +19,22 @@ const UserDetail: React.FC<{ const history = useHistory(); const handleStudentClick = (studentId: string) => { - history.replace(PAGES.STUDENT_PROFILE, { - classDoc: classDoc, - studentId: studentId, + history.replace({ + ...parsePath(PAGES.STUDENT_PROFILE), + state: { + classDoc: classDoc, + studentId: studentId, + }, }); }; const handleTeacherClick = () => { localStorage.setItem(CURRENT_TEACHER, JSON.stringify(user)); - history.replace(PAGES.TEACHER_PROFILE, { - classDoc: classDoc, - school: schoolDoc, + history.replace({ + ...parsePath(PAGES.TEACHER_PROFILE), + state: { + classDoc: classDoc, + school: schoolDoc, + }, }); }; const getRandomAvatar = () => { diff --git a/src/teachers-module/pages/AddSchoolUser.tsx b/src/teachers-module/pages/AddSchoolUser.tsx index 8ec32e6fa6..8ee4709687 100644 --- a/src/teachers-module/pages/AddSchoolUser.tsx +++ b/src/teachers-module/pages/AddSchoolUser.tsx @@ -15,6 +15,7 @@ import { t } from 'i18next'; import { RoleType } from '../../interface/modelInterfaces'; import CommonDialogBox from '../../common/CommonDialogBox'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const AddSchoolUser: React.FC = () => { const history = useHistory(); @@ -53,9 +54,12 @@ const AddSchoolUser: React.FC = () => { }; const onBackButtonClick = () => { - history.replace(`${PAGES.SCHOOL_USERS}?tab=${tempTabName}`, { - school: school, - role: role, + history.replace({ + ...parsePath(`${PAGES.SCHOOL_USERS}?tab=${tempTabName}`), + state: { + school: school, + role: role, + }, }); }; @@ -107,9 +111,12 @@ const AddSchoolUser: React.FC = () => { await api.updateSchoolLastModified(school.id); await api.updateUserLastModified(user.id); - history.replace(`${PAGES.SCHOOL_USERS}?tab=${tempTabName}`, { - school: school, - role: role, + history.replace({ + ...parsePath(`${PAGES.SCHOOL_USERS}?tab=${tempTabName}`), + state: { + school: school, + role: role, + }, }); } catch (error) { logger.error('Failed to add user to school', error); diff --git a/src/teachers-module/pages/AddStudent.tsx b/src/teachers-module/pages/AddStudent.tsx index beb52a980f..7e2c7770ee 100644 --- a/src/teachers-module/pages/AddStudent.tsx +++ b/src/teachers-module/pages/AddStudent.tsx @@ -15,6 +15,7 @@ import ProfileDetails from '../components/library/ProfileDetails'; import Loading from '../../components/Loading'; import logger from '../../utility/logger'; import { useFeatureValue } from '@growthbook/growthbook-react'; +import { parsePath } from 'history'; const AddStudent: React.FC = () => { const history = useHistory(); @@ -69,7 +70,7 @@ const AddStudent: React.FC = () => { }; const handleBack = () => { - history.replace(PAGES.CLASS_USERS, classDoc); + history.replace({ ...parsePath(PAGES.CLASS_USERS), state: classDoc }); }; const fetchClassDetails = async () => { try { diff --git a/src/teachers-module/pages/AddTeacher.tsx b/src/teachers-module/pages/AddTeacher.tsx index d9425bfeb9..1fe326e0e7 100644 --- a/src/teachers-module/pages/AddTeacher.tsx +++ b/src/teachers-module/pages/AddTeacher.tsx @@ -8,6 +8,7 @@ import './AddTeacher.css'; import { ServiceConfig } from '../../services/ServiceConfig'; import { t } from 'i18next'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const AddTeacher: React.FC = () => { const history = useHistory(); @@ -35,7 +36,10 @@ const AddTeacher: React.FC = () => { }; const onBackButtonClick = () => { - history.replace(`${PAGES.CLASS_USERS}?tab=Teachers`, classDoc); + history.replace({ + ...parsePath(`${PAGES.CLASS_USERS}?tab=Teachers`), + state: classDoc, + }); }; const handleSearch = async () => { @@ -84,7 +88,10 @@ const AddTeacher: React.FC = () => { await api.updateSchoolLastModified(school.id); await api.updateClassLastModified(classDoc.id); await api.updateUserLastModified(user.id); - history.replace(`${PAGES.CLASS_USERS}?tab=Teachers`, classDoc); + history.replace({ + ...parsePath(`${PAGES.CLASS_USERS}?tab=Teachers`), + state: classDoc, + }); } catch (error) { logger.error('Failed to add teacher', error); } finally { diff --git a/src/teachers-module/pages/ClassProfile.tsx b/src/teachers-module/pages/ClassProfile.tsx index 06e64886e8..0ea605669d 100644 --- a/src/teachers-module/pages/ClassProfile.tsx +++ b/src/teachers-module/pages/ClassProfile.tsx @@ -7,6 +7,8 @@ import './ClassProfile.css'; import { t } from 'i18next'; import DeleteClassDialog from '../components/classComponents/DeleteClassDialog'; import { schoolUtil } from '../../utility/schoolUtil'; +import { parsePath } from 'history'; + const ClassProfile: FC = () => { const history = useHistory(); const location = useLocation(); @@ -26,9 +28,12 @@ const ClassProfile: FC = () => { }; const handleEditClass = () => { - history.replace(PAGES.EDIT_CLASS, { - school: currentSchool, - classDoc: currentClass, + history.replace({ + ...parsePath(PAGES.EDIT_CLASS), + state: { + school: currentSchool, + classDoc: currentClass, + }, }); }; diff --git a/src/teachers-module/pages/ClassUsers.tsx b/src/teachers-module/pages/ClassUsers.tsx index 1e80a499e8..8e4b4614c5 100644 --- a/src/teachers-module/pages/ClassUsers.tsx +++ b/src/teachers-module/pages/ClassUsers.tsx @@ -18,6 +18,7 @@ import { Util } from '../../utility/util'; import Header from '../components/homePage/Header'; import UserList from '../components/studentProfile/UserList'; import './ClassUsers.css'; +import { parsePath } from 'history'; const ClassUsers: React.FC = () => { const history = useHistory(); @@ -73,15 +74,21 @@ const ClassUsers: React.FC = () => { action_type: AUTO_USER_ACTION_TYPES.ADD_STUDENT, }); } - history.replace(PAGES.ADD_STUDENT, { - classDoc: classData, - school: currentSchool, + history.replace({ + ...parsePath(PAGES.ADD_STUDENT), + state: { + classDoc: classData, + school: currentSchool, + }, }); }; const addTeacher = () => { - history.replace(PAGES.ADD_TEACHER, { - classDoc: classData, - school: currentSchool, + history.replace({ + ...parsePath(PAGES.ADD_TEACHER), + state: { + classDoc: classData, + school: currentSchool, + }, }); }; diff --git a/src/teachers-module/pages/DashBoardDetails.tsx b/src/teachers-module/pages/DashBoardDetails.tsx index cd71bca1cc..46a342fd0b 100644 --- a/src/teachers-module/pages/DashBoardDetails.tsx +++ b/src/teachers-module/pages/DashBoardDetails.tsx @@ -12,6 +12,7 @@ import { t } from 'i18next'; import DashBoardStudentProgres from '../components/homePage/DashBoardStudentProgres'; import { Util } from '../../utility/util'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; interface DashBoardDetailsProps {} type DashBoardDetailsState = { @@ -53,7 +54,10 @@ const DashBoardDetails: React.FC = ({}) => {
{ - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); }} showSchool={true} showClass={true} diff --git a/src/teachers-module/pages/DisplayClasses.tsx b/src/teachers-module/pages/DisplayClasses.tsx index b73e57474a..940243001d 100644 --- a/src/teachers-module/pages/DisplayClasses.tsx +++ b/src/teachers-module/pages/DisplayClasses.tsx @@ -8,6 +8,7 @@ import { PAGES, TableTypes } from '../../common/constants'; import BackButton from '../../components/common/BackButton'; import './DisplayClasses.css'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const DisplayClasses: FC = () => { const history = useHistory(); @@ -40,10 +41,13 @@ const DisplayClasses: FC = () => { const schoolCourses = await api.getCoursesBySchoolId(tempSchool.id); if (schoolCourses.length === 0) { - history.replace(PAGES.SUBJECTS_PAGE, { - schoolId: tempSchool.id, - origin: PAGES.DISPLAY_CLASSES, - isSelect: true, + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { + schoolId: tempSchool.id, + origin: PAGES.DISPLAY_CLASSES, + isSelect: true, + }, }); return; } @@ -53,9 +57,12 @@ const DisplayClasses: FC = () => { user.id, ); if (fetchedClasses.length === 0) { - history.replace(PAGES.ADD_CLASS, { - school: currentSchool, - origin: PAGES.DISPLAY_CLASSES, + history.replace({ + ...parsePath(PAGES.ADD_CLASS), + state: { + school: currentSchool, + origin: PAGES.DISPLAY_CLASSES, + }, }); return; } @@ -74,10 +81,13 @@ const DisplayClasses: FC = () => { ); if (classWithoutSubjects) { - history.replace(PAGES.SUBJECTS_PAGE, { - classId: classWithoutSubjects.classId, - origin: PAGES.DISPLAY_CLASSES, - isSelect: true, + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { + classId: classWithoutSubjects.classId, + origin: PAGES.DISPLAY_CLASSES, + isSelect: true, + }, }); Util.clearNavigationState(); return; @@ -90,7 +100,7 @@ const DisplayClasses: FC = () => { const handleClassSelection = (selectedClass: TableTypes<'class'>) => { Util.setCurrentClass(selectedClass); - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ ...parsePath(PAGES.HOME_PAGE), state: { tabValue: 0 } }); }; return ( diff --git a/src/teachers-module/pages/DisplaySchools.tsx b/src/teachers-module/pages/DisplaySchools.tsx index e3a992a196..6867369889 100644 --- a/src/teachers-module/pages/DisplaySchools.tsx +++ b/src/teachers-module/pages/DisplaySchools.tsx @@ -27,6 +27,7 @@ import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import { AuthState } from '../../redux/slices/auth/authSlice'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; interface SchoolWithRole { school: TableTypes<'school'>; @@ -172,15 +173,27 @@ const DisplaySchools: FC = () => { existingRequest?.request_status === STATUS.REQUESTED || existingRequest?.request_status === STATUS.FLAGGED ) { - history.replace(PAGES.POST_SUCCESS, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.POST_SUCCESS), + state: { tabValue: 0 }, + }); } else if (existingRequest?.request_status === STATUS.REJECTED) { - history.replace(PAGES.SEARCH_SCHOOL, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.SEARCH_SCHOOL), + state: { tabValue: 0 }, + }); } else if (existingRequest?.request_status === STATUS.APPROVED) { // If approved but school not in list, go to Search School to avoid flicker/loop - history.replace(PAGES.SEARCH_SCHOOL, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.SEARCH_SCHOOL), + state: { tabValue: 0 }, + }); } else { - history.replace(PAGES.SEARCH_SCHOOL, { - origin: PAGES.DISPLAY_SCHOOLS, + history.replace({ + ...parsePath(PAGES.SEARCH_SCHOOL), + state: { + origin: PAGES.DISPLAY_SCHOOLS, + }, }); } setLoading(false); @@ -261,12 +274,18 @@ const DisplaySchools: FC = () => { localStorage.setItem(USER_SELECTION_STAGE, JSON.stringify(true)); const tempClass = Util.getCurrentClass(); if (tempClass) { - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); } else { const classes = await getClasses(school.school.id, currentUser?.id); if (classes.length > 0) { Util.setCurrentClass(classes[0]); - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); } } void Util.validateCurrentSchoolContext(); @@ -304,8 +323,11 @@ const DisplaySchools: FC = () => {
- history.replace(PAGES.REQ_ADD_SCHOOL, { - origin: PAGES.DISPLAY_SCHOOLS, + history.replace({ + ...parsePath(PAGES.REQ_ADD_SCHOOL), + state: { + origin: PAGES.DISPLAY_SCHOOLS, + }, }) } > diff --git a/src/teachers-module/pages/EditClass.tsx b/src/teachers-module/pages/EditClass.tsx index 384d7a8466..076b30f2b5 100644 --- a/src/teachers-module/pages/EditClass.tsx +++ b/src/teachers-module/pages/EditClass.tsx @@ -18,6 +18,7 @@ import { getGradeNameFromStandard, getStandardFromClassName, } from '../../utility/classGradeMapper'; +import { parsePath } from 'history'; type LocationState = { school?: TableTypes<'school'>; @@ -98,10 +99,13 @@ const EditClass: FC = () => { ); setIsSaving(false); Util.setNavigationState(School_Creation_Stages.CLASS_COURSE); - history.replace(PAGES.SUBJECTS_PAGE, { - classId: newClass.id, - origin: PAGES.ADD_CLASS, - isSelect: true, + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { + classId: newClass.id, + origin: PAGES.ADD_CLASS, + isSelect: true, + }, }); } } catch (error) { @@ -135,20 +139,25 @@ const EditClass: FC = () => { if (paramOrigin === PAGES.MANAGE_CLASS) { Util.setPathToBackButton(PAGES.MANAGE_CLASS, history); } else if (paramOrigin != PAGES.SUBJECTS_PAGE) { - history.replace( - paramOrigin === PAGES.HOME_PAGE - ? PAGES.HOME_PAGE - : PAGES.DISPLAY_SCHOOLS, - paramOrigin === PAGES.HOME_PAGE ? { tabValue: 0 } : null, - ); + history.replace({ + ...parsePath( + paramOrigin === PAGES.HOME_PAGE + ? PAGES.HOME_PAGE + : PAGES.DISPLAY_SCHOOLS, + ), + state: paramOrigin === PAGES.HOME_PAGE ? { tabValue: 0 } : null, + }); return; } else if (paramOrigin === PAGES.SUBJECTS_PAGE) { if (!currentSchool) return; Util.setNavigationState(School_Creation_Stages.SCHOOL_COURSE); - history.replace(PAGES.SUBJECTS_PAGE, { - schoolId: currentSchool.id, - origin: PAGES.DISPLAY_SCHOOLS, - isSelect: true, + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { + schoolId: currentSchool.id, + origin: PAGES.DISPLAY_SCHOOLS, + isSelect: true, + }, }); return; } diff --git a/src/teachers-module/pages/EditSchool.tsx b/src/teachers-module/pages/EditSchool.tsx index 23ab8d9939..bff8d84eac 100644 --- a/src/teachers-module/pages/EditSchool.tsx +++ b/src/teachers-module/pages/EditSchool.tsx @@ -12,6 +12,8 @@ import { RoleType } from '../../interface/modelInterfaces'; import { Util } from '../../utility/util'; import ProfileDetails from '../components/library/ProfileDetails'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; + interface LocationState { school?: SchoolWithRole['school']; role?: RoleType; @@ -134,17 +136,19 @@ const EditSchool: React.FC = () => { }; const onBackButtonClick = () => { - history.replace( - prevOrigin === PAGES.DISPLAY_SCHOOLS - ? PAGES.DISPLAY_SCHOOLS - : isEditMode && !navigationState - ? PAGES.SCHOOL_PROFILE - : PAGES.MANAGE_SCHOOL, - { + history.replace({ + ...parsePath( + prevOrigin === PAGES.DISPLAY_SCHOOLS + ? PAGES.DISPLAY_SCHOOLS + : isEditMode && !navigationState + ? PAGES.SCHOOL_PROFILE + : PAGES.MANAGE_SCHOOL, + ), + state: { school: school, role: role, }, - ); + }); }; const [profilePic, setProfilePic] = useState(null); diff --git a/src/teachers-module/pages/HomePage.tsx b/src/teachers-module/pages/HomePage.tsx index 1f30a481f3..b473971364 100644 --- a/src/teachers-module/pages/HomePage.tsx +++ b/src/teachers-module/pages/HomePage.tsx @@ -43,6 +43,7 @@ import Library from '../components/library/Library'; import ReportTable from '../components/reports/ReportsTable'; import './HomePage.css'; import { format, subDays } from 'date-fns'; +import { parsePath } from 'history'; const HomePage: React.FC = () => { const history = useHistory(); @@ -274,7 +275,7 @@ const HomePage: React.FC = () => { const handleLibraryBack = () => { setShowAssignOptionsScreen(true); setTabValue(2); - history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + history.replace({ ...parsePath(PAGES.HOME_PAGE), state: { tabValue: 2 } }); }; const isLibraryTab = tabValue === 1; const footerTabValue = tabValue === 1 ? 2 : tabValue; diff --git a/src/teachers-module/pages/LessonDetails.tsx b/src/teachers-module/pages/LessonDetails.tsx index 2c25c90083..d4fe505eac 100644 --- a/src/teachers-module/pages/LessonDetails.tsx +++ b/src/teachers-module/pages/LessonDetails.tsx @@ -20,6 +20,8 @@ import { Capacitor } from '@capacitor/core'; import { ScreenOrientation } from '@capacitor/screen-orientation'; import { useOnlineOfflineErrorMessageHandler } from '../../common/onlineOfflineErrorMessageHandler'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; + interface LessonDetailsProps {} type LessonDetailsState = { course?: TableTypes<'course'>; @@ -101,9 +103,11 @@ const LessonDetails: React.FC = ({}) => { }); return; } - history.push( - PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, - { + history.push({ + ...parsePath( + PAGES.LIVE_QUIZ_GAME + `?lessonId=${lesson.cocos_lesson_id}`, + ), + state: { courseId: course?.id, lesson: JSON.stringify(lesson), selectedLesson: selectedLessonMap, @@ -111,22 +115,25 @@ const LessonDetails: React.FC = ({}) => { returnState: lessonDetailsReturnState, source: SOURCE.TEACHER_MODE, }, - ); + }); } else { const playableLessonId = Util.getLessonBundleId(lesson); if (!playableLessonId) { return; } const parmas = `?courseid=${lesson.cocos_subject_code}&chapterid=${lesson.cocos_chapter_code}&lessonid=${playableLessonId}`; - history.push(PAGES.LIDO_PLAYER + parmas, { - lessonId: playableLessonId, - courseDocId: course?.id, - course: JSON.stringify(course!), - lesson: JSON.stringify(lesson), - selectedLesson: selectedLessonMap, - from: history.location.pathname + `?${CONTINUE}=true`, - returnState: lessonDetailsReturnState, - source: SOURCE.TEACHER_MODE, + history.push({ + ...parsePath(PAGES.LIDO_PLAYER + parmas), + state: { + lessonId: playableLessonId, + courseDocId: course?.id, + course: JSON.stringify(course!), + lesson: JSON.stringify(lesson), + selectedLesson: selectedLessonMap, + from: history.location.pathname + `?${CONTINUE}=true`, + returnState: lessonDetailsReturnState, + source: SOURCE.TEACHER_MODE, + }, }); } }; @@ -256,11 +263,17 @@ const LessonDetails: React.FC = ({}) => { isBackButton={true} onButtonClick={() => { course - ? history.replace(state.from || PAGES.SEARCH_LESSON, { - course: course, - chapterId: chapterId, + ? history.replace({ + ...parsePath(state.from || PAGES.SEARCH_LESSON), + state: { + course: course, + chapterId: chapterId, + }, }) - : history.replace(PAGES.HOME_PAGE, { tabValue: 1 }); + : history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 1 }, + }); }} showSideMenu={false} customText={t('Learning Outcome') ?? 'Learning Outcome'} @@ -389,11 +402,17 @@ const LessonDetails: React.FC = ({}) => { assignments={assignmentCount} onClick={() => { course - ? history.replace(PAGES.SHOW_CHAPTERS, { - course, - chapterId, + ? history.replace({ + ...parsePath(PAGES.SHOW_CHAPTERS), + state: { + course, + chapterId, + }, }) - : history.replace(PAGES.HOME_PAGE, { tabValue: 2 }); + : history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 2 }, + }); }} />
diff --git a/src/teachers-module/pages/ManageClass.tsx b/src/teachers-module/pages/ManageClass.tsx index b0771379e6..3e737aee78 100644 --- a/src/teachers-module/pages/ManageClass.tsx +++ b/src/teachers-module/pages/ManageClass.tsx @@ -14,6 +14,7 @@ import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import { AuthState } from '../../redux/slices/auth/authSlice'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const CLASS_CREATION_ROLES = [ RoleType.SUPER_ADMIN, @@ -73,8 +74,11 @@ const ManageClass: React.FC = () => { !!currentSchoolRole && CLASS_CREATION_ROLES.includes(currentSchoolRole); const onBackButtonClick = () => { - history.replace(PAGES.HOME_PAGE, { - tabValue: 0, + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { + tabValue: 0, + }, }); }; useEffect(() => { @@ -118,7 +122,12 @@ const ManageClass: React.FC = () => { {canCreate && !isExternalUser && ( { - history.replace(PAGES.ADD_CLASS, { origin: PAGES.MANAGE_CLASS }); + history.replace({ + ...parsePath(PAGES.ADD_CLASS), + state: { + origin: PAGES.MANAGE_CLASS, + }, + }); }} /> )} diff --git a/src/teachers-module/pages/ManageSchools.tsx b/src/teachers-module/pages/ManageSchools.tsx index 4cdd1bec29..a0494bb43f 100644 --- a/src/teachers-module/pages/ManageSchools.tsx +++ b/src/teachers-module/pages/ManageSchools.tsx @@ -20,6 +20,7 @@ import logger from '../../utility/logger'; import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import { AuthState } from '../../redux/slices/auth/authSlice'; +import { parsePath } from 'history'; const PAGE_SIZE = 20; const SEARCH_DEBOUNCE_MS = 500; @@ -123,8 +124,11 @@ const ManageSchools: React.FC = () => { }; const onBackButtonClick = () => { - history.replace(PAGES.HOME_PAGE, { - tabValue: 0, + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { + tabValue: 0, + }, }); }; diff --git a/src/teachers-module/pages/ReqEditSchool.tsx b/src/teachers-module/pages/ReqEditSchool.tsx index 9d58600dec..d055e9fec1 100644 --- a/src/teachers-module/pages/ReqEditSchool.tsx +++ b/src/teachers-module/pages/ReqEditSchool.tsx @@ -22,6 +22,7 @@ import { useOnlineOfflineErrorMessageHandler } from '../../common/onlineOfflineE import logger from '../../utility/logger'; import { logAuthDebug } from '../../utility/authDebug'; import { getAppPathname } from '../../utility/routerLocation'; +import { parsePath } from 'history'; interface LocationState { school?: SchoolWithRole['school']; role?: RoleType; @@ -89,17 +90,19 @@ const ReqEditSchool: React.FC = () => { }; const onBackButtonClick = () => { - history.replace( - prevOrigin === PAGES.DISPLAY_SCHOOLS - ? PAGES.DISPLAY_SCHOOLS - : isEditMode && !navigationState - ? PAGES.SCHOOL_PROFILE - : PAGES.MANAGE_SCHOOL, - { + history.replace({ + ...parsePath( + prevOrigin === PAGES.DISPLAY_SCHOOLS + ? PAGES.DISPLAY_SCHOOLS + : isEditMode && !navigationState + ? PAGES.SCHOOL_PROFILE + : PAGES.MANAGE_SCHOOL, + ), + state: { school: school, role: role, }, - ); + }); }; const [profilePic, setProfilePic] = useState(null); diff --git a/src/teachers-module/pages/SchoolUsers.tsx b/src/teachers-module/pages/SchoolUsers.tsx index b31dbc199d..ad0b2c7fa1 100644 --- a/src/teachers-module/pages/SchoolUsers.tsx +++ b/src/teachers-module/pages/SchoolUsers.tsx @@ -9,6 +9,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import { RoleType } from '../../interface/modelInterfaces'; import SchoolUserList from '../components/schoolUsers/SchoolUserList'; import { IonPage } from '@ionic/react'; +import { parsePath } from 'history'; const SchoolUsers: React.FC = () => { const history = useHistory(); @@ -58,21 +59,30 @@ const SchoolUsers: React.FC = () => { setSelectedTab(SCHOOL_USERS.PRINCIPALS); }; const addPrincipal = () => { - history.replace(PAGES.ADD_PRINCIPAL, { - school: school, - role: role, + history.replace({ + ...parsePath(PAGES.ADD_PRINCIPAL), + state: { + school: school, + role: role, + }, }); }; const addCoordinator = () => { - history.replace(PAGES.ADD_COORDINATOR, { - school: school, - role: role, + history.replace({ + ...parsePath(PAGES.ADD_COORDINATOR), + state: { + school: school, + role: role, + }, }); }; const addSponsor = () => { - history.replace(PAGES.ADD_SPONSOR, { - school: school, - role: role, + history.replace({ + ...parsePath(PAGES.ADD_SPONSOR), + state: { + school: school, + role: role, + }, }); }; diff --git a/src/teachers-module/pages/SearchLessons.tsx b/src/teachers-module/pages/SearchLessons.tsx index b701451bbf..8820bd776e 100644 --- a/src/teachers-module/pages/SearchLessons.tsx +++ b/src/teachers-module/pages/SearchLessons.tsx @@ -18,6 +18,7 @@ import ChapterWiseLessons from '../components/ChapterWiseLessons'; import { readAssignmentCartFromStorage } from './AssignmentCartStorage'; import logger from '../../utility/logger'; import AssignedVisibilityToggle from '../components/AssignedVisibilityToggle'; +import { parsePath } from 'history'; type LessonMeta = { chapterId: string | null; @@ -654,7 +655,12 @@ const SearchLesson: React.FC = () => {
history.replace(PAGES.HOME_PAGE, { tabValue: 1 })} + onButtonClick={() => + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 1 }, + }) + } customText={t('Search') ?? 'Search'} schoolName={currentSchool?.name} className={currentClass?.name} diff --git a/src/teachers-module/pages/SearchSchool.tsx b/src/teachers-module/pages/SearchSchool.tsx index dd1e5cee1b..2b604aa707 100644 --- a/src/teachers-module/pages/SearchSchool.tsx +++ b/src/teachers-module/pages/SearchSchool.tsx @@ -31,6 +31,7 @@ import { t } from 'i18next'; import { useHistory } from 'react-router'; import { schoolUtil } from '../../utility/schoolUtil'; import Header from '../components/homePage/Header'; +import { parsePath } from 'history'; const PAGE_LIMIT = 50; @@ -75,9 +76,15 @@ const SearchSchool: FC = () => { currentUser?.id as string, ); if (existingRequest?.request_status === STATUS.REQUESTED) { - history.replace(PAGES.POST_SUCCESS, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.POST_SUCCESS), + state: { tabValue: 0 }, + }); } else { - history.replace(PAGES.SEARCH_SCHOOL, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.SEARCH_SCHOOL), + state: { tabValue: 0 }, + }); } }; useEffect(() => { diff --git a/src/teachers-module/pages/ShowChapters.tsx b/src/teachers-module/pages/ShowChapters.tsx index cebfb67f06..fd3bc056cc 100644 --- a/src/teachers-module/pages/ShowChapters.tsx +++ b/src/teachers-module/pages/ShowChapters.tsx @@ -20,6 +20,7 @@ import { } from './ShowChaptersLogic'; import logger from '../../utility/logger'; import AssignedVisibilityToggle from '../components/AssignedVisibilityToggle'; +import { parsePath } from 'history'; const ShowChapters: React.FC = () => { const [currentClass, setCurrentClass] = useState | null>( @@ -176,14 +177,17 @@ const ShowChapters: React.FC = () => { lesson: TableTypes<'lesson'>, chapter: TableTypes<'chapter'>, ) => { - history.replace(PAGES.LESSON_DETAILS, { - course: course, - lesson: lesson, - chapterId: chapter.id, - selectedLesson: selectedLesson, - chapterName: chapter.name, - gradeName: selectedCourseGrade, - from: PAGES.SHOW_CHAPTERS, + history.replace({ + ...parsePath(PAGES.LESSON_DETAILS), + state: { + course: course, + lesson: lesson, + chapterId: chapter.id, + selectedLesson: selectedLesson, + chapterName: chapter.name, + gradeName: selectedCourseGrade, + from: PAGES.SHOW_CHAPTERS, + }, }); }; // if (lessonIds !== undefined) { @@ -387,7 +391,10 @@ const ShowChapters: React.FC = () => {
{ - history.replace(PAGES.HOME_PAGE, { tabValue: 1 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 1 }, + }); }} customText="Library" showSearchIcon={true} diff --git a/src/teachers-module/pages/ShowStudentsInAssignmentPage.tsx b/src/teachers-module/pages/ShowStudentsInAssignmentPage.tsx index 7454f86b9a..e21a27a65c 100644 --- a/src/teachers-module/pages/ShowStudentsInAssignmentPage.tsx +++ b/src/teachers-module/pages/ShowStudentsInAssignmentPage.tsx @@ -6,6 +6,7 @@ import './ShowStudentsInAssignmentPage.css'; import CreateSelectedAssignment from '../components/homePage/assignment/CreateSelectedAssignment'; import { Util } from '../../utility/util'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const ShowStudentsInAssignmentPage: React.FC = () => { const [currentUser, setCurrentUser] = useState | null>( @@ -61,7 +62,10 @@ const ShowStudentsInAssignmentPage: React.FC = () => { return; } if (fromPage === PAGES.QR_ASSIGNMENTS && qrAssignmentNavigationState) { - history.replace(PAGES.QR_ASSIGNMENTS, qrAssignmentNavigationState); + history.replace({ + ...parsePath(PAGES.QR_ASSIGNMENTS), + state: qrAssignmentNavigationState, + }); return; } history.replace(PAGES.TEACHER_ASSIGNMENT); diff --git a/src/teachers-module/pages/StudentProfile.tsx b/src/teachers-module/pages/StudentProfile.tsx index 8aa2756278..89c98c5750 100644 --- a/src/teachers-module/pages/StudentProfile.tsx +++ b/src/teachers-module/pages/StudentProfile.tsx @@ -10,6 +10,7 @@ import { Util } from '../../utility/util'; import { subDays } from 'date-fns'; import { useHistory } from 'react-router-dom'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; const StudentProfile: React.FC = () => { const history = useHistory(); @@ -36,12 +37,15 @@ const StudentProfile: React.FC = () => { const handleViewProgressClick = () => { var startDate = subDays(new Date(), 6); var endDate = new Date(); - history.replace(PAGES.STUDENT_REPORT, { - student: student, - startDate: startDate, - endDate: endDate, - isStudentProfilePage: true, - classDoc: tempClass, + history.replace({ + ...parsePath(PAGES.STUDENT_REPORT), + state: { + student: student, + startDate: startDate, + endDate: endDate, + isStudentProfilePage: true, + classDoc: tempClass, + }, }); }; @@ -65,7 +69,7 @@ const StudentProfile: React.FC = () => { }; const onBackButtonClick = () => { - history.replace(PAGES.CLASS_USERS, currentClass); + history.replace({ ...parsePath(PAGES.CLASS_USERS), state: currentClass }); }; const handleUpdateClick = async () => { diff --git a/src/teachers-module/pages/StudentReport.tsx b/src/teachers-module/pages/StudentReport.tsx index 6d7e9a792c..77af962228 100644 --- a/src/teachers-module/pages/StudentReport.tsx +++ b/src/teachers-module/pages/StudentReport.tsx @@ -13,6 +13,7 @@ import { PAGES, TableTypes } from '../../common/constants'; import { ServiceConfig } from '../../services/ServiceConfig'; import { ClassUtil } from '../../utility/classUtil'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; type StudentReportLocationState = { student?: TableTypes<'user'>; @@ -194,20 +195,29 @@ const StudentReport: React.FC = () => { }; const handleBackButton = () => { if (isStudentProfilePage) { - history.replace(PAGES.STUDENT_PROFILE, { - studentId: student.id, - classDoc: tempClass, + history.replace({ + ...parsePath(PAGES.STUDENT_PROFILE), + state: { + studentId: student.id, + classDoc: tempClass, + }, }); } else if (fromDashboardBand) { - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); } else { - history.replace(PAGES.HOME_PAGE, { - tabValue: 3, - startDate: locationState.startDate, - endDate: locationState.endDate, - selectedType: locationState.selectedType, - isAssignments: locationState.isAssignments, - sortType: locationState.sortType, + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { + tabValue: 3, + startDate: locationState.startDate, + endDate: locationState.endDate, + selectedType: locationState.selectedType, + isAssignments: locationState.isAssignments, + sortType: locationState.sortType, + }, }); } }; diff --git a/src/teachers-module/pages/SubjectSelection.tsx b/src/teachers-module/pages/SubjectSelection.tsx index 1e07d304aa..dd380fdc4c 100644 --- a/src/teachers-module/pages/SubjectSelection.tsx +++ b/src/teachers-module/pages/SubjectSelection.tsx @@ -22,6 +22,7 @@ import { AuthState } from '../../redux/slices/auth/authSlice'; import { RootState } from '../../redux/store'; import logger from '../../utility/logger'; import { getCachedImageSrc } from '../../utility/imageCache'; +import { parsePath } from 'history'; interface CurriculumWithCourses { curriculum: { id: string; name: string; grade?: string }; @@ -454,22 +455,31 @@ const SubjectSelection: React.FC = () => { const finalRole: RoleType = userRole ?? RoleType.PRINCIPAL; if (previousOrigin === PAGES.DISPLAY_SCHOOLS) { Util.setNavigationState(School_Creation_Stages.CREATE_CLASS); - history.replace(PAGES.ADD_CLASS, { - school: currentSchool, - origin: PAGES.SUBJECTS_PAGE, + history.replace({ + ...parsePath(PAGES.ADD_CLASS), + state: { + school: currentSchool, + origin: PAGES.SUBJECTS_PAGE, + }, }); } else if (previousOrigin === PAGES.HOME_PAGE) { Util.setCurrentSchool(currentSchool!, finalRole); Util.setCurrentClass(currentClass!); Util.clearNavigationState(); - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); void Util.validateCurrentSchoolContext(); } else { if (navigationState?.stage === School_Creation_Stages.CLASS_COURSE) { Util.setCurrentSchool(currentSchool!, finalRole); Util.setCurrentClass(currentClass!); Util.clearNavigationState(); - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 0 }, + }); void Util.validateCurrentSchoolContext(); } setIsSelecting(false); @@ -571,9 +581,12 @@ const SubjectSelection: React.FC = () => { } if (navigationState?.stage === School_Creation_Stages.CLASS_COURSE) { Util.setNavigationState(School_Creation_Stages.CREATE_CLASS); - history.replace(PAGES.EDIT_CLASS, { - school: currentSchool, - classDoc: currentClass, + history.replace({ + ...parsePath(PAGES.EDIT_CLASS), + state: { + school: currentSchool, + classDoc: currentClass, + }, }); } else { paramSchoolId diff --git a/src/teachers-module/pages/TeacherLibraryAssignmentsLogic.ts b/src/teachers-module/pages/TeacherLibraryAssignmentsLogic.ts index b1214b1b9e..bc21a9189c 100644 --- a/src/teachers-module/pages/TeacherLibraryAssignmentsLogic.ts +++ b/src/teachers-module/pages/TeacherLibraryAssignmentsLogic.ts @@ -12,6 +12,7 @@ import { ServiceConfig } from '../../services/ServiceConfig'; import { Util } from '../../utility/util'; import { TeacherAssignmentPageType } from '../components/homePage/assignment/TeacherAssignment'; import logger from '../../utility/logger'; +import { parsePath } from 'history'; export interface AssignmentLessonItem { id: string; @@ -49,7 +50,10 @@ export const useTeacherLibraryAssignmentsLogic = () => { const currUser = await auth.getCurrentUser(); if (!currentClass?.id || !currUser?.id) { - history.replace(PAGES.HOME_PAGE, { tabValue: 1 }); + history.replace({ + ...parsePath(PAGES.HOME_PAGE), + state: { tabValue: 1 }, + }); return; } @@ -225,10 +229,10 @@ export const useTeacherLibraryAssignmentsLogic = () => { return; } - history.replace( - PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, - buildAssignmentPayload(), - ); + history.replace({ + ...parsePath(PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE), + state: buildAssignmentPayload(), + }); }; const handleBackButtonClick = () => { @@ -236,7 +240,7 @@ export const useTeacherLibraryAssignmentsLogic = () => { history.goBack(); return; } - history.replace(PAGES.HOME_PAGE, { tabValue: 1 }); + history.replace({ ...parsePath(PAGES.HOME_PAGE), state: { tabValue: 1 } }); }; const getSelectedCount = (group: AssignmentCourseGroup): number => diff --git a/src/teachers-module/pages/TeacherProfile.tsx b/src/teachers-module/pages/TeacherProfile.tsx index 3bd5958e23..f18d24b76d 100644 --- a/src/teachers-module/pages/TeacherProfile.tsx +++ b/src/teachers-module/pages/TeacherProfile.tsx @@ -5,6 +5,7 @@ import Header from '../components/homePage/Header'; import { IonPage } from '@ionic/react'; import TeacherProfileSection from '../components/addTeacher/TeacherProfileSection'; import { useHistory, useLocation } from 'react-router-dom'; +import { parsePath } from 'history'; const TeacherProfile: React.FC = () => { const localTeacher = localStorage.getItem(CURRENT_TEACHER); @@ -17,7 +18,10 @@ const TeacherProfile: React.FC = () => { const { classDoc, school } = location.state || {}; const onBackButtonClick = () => { - history.replace(`${PAGES.CLASS_USERS}?tab=Teachers`, classDoc); + history.replace({ + ...parsePath(`${PAGES.CLASS_USERS}?tab=Teachers`), + state: classDoc, + }); }; return ( diff --git a/src/teachers-module/pages/UserProfile.tsx b/src/teachers-module/pages/UserProfile.tsx index bd15147e32..c902389de6 100644 --- a/src/teachers-module/pages/UserProfile.tsx +++ b/src/teachers-module/pages/UserProfile.tsx @@ -11,6 +11,7 @@ import Header from '../components/homePage/Header'; import ProfileDetails from '../components/library/ProfileDetails'; import UserProfileSection from '../components/UserProfileSection'; import './UserProfile.css'; +import { parsePath } from 'history'; const UserProfile: React.FC = () => { const history = useHistory(); @@ -154,7 +155,7 @@ const UserProfile: React.FC = () => { const placeholderImgBase64 = 'data:image/png;base64,....'; const onBackButtonClick = () => { - history.replace(PAGES.HOME_PAGE, { tabValue: 0 }); + history.replace({ ...parsePath(PAGES.HOME_PAGE), state: { tabValue: 0 } }); }; return (
diff --git a/src/utility/util.ts b/src/utility/util.ts index 9628409c28..63d79c1ae5 100644 --- a/src/utility/util.ts +++ b/src/utility/util.ts @@ -129,6 +129,7 @@ import { getAppSearchParams, replaceAppUrl, } from './routerLocation'; +import { parsePath } from 'history'; type LessonBundleDownloadOptions = { lessonId: string; @@ -2248,10 +2249,13 @@ export class Util { origin: PAGES, classId?: string, ) { - history.replace(redirectPage, { - classId: classId, - origin: origin, - isSelect: true, + history.replace({ + ...parsePath(redirectPage), + state: { + classId: classId, + origin: origin, + isSelect: true, + }, }); } public static async handleClassAndSubjects( @@ -2265,18 +2269,24 @@ export class Util { const schoolCourses = await api.getCoursesBySchoolId(schoolId); if (schoolCourses.length === 0) { this.setNavigationState(School_Creation_Stages.SCHOOL_COURSE); - history.replace(PAGES.SUBJECTS_PAGE, { - schoolId: schoolId, - origin: originPage, - isSelect: true, + history.replace({ + ...parsePath(PAGES.SUBJECTS_PAGE), + state: { + schoolId: schoolId, + origin: originPage, + isSelect: true, + }, }); return; } const fetchedClasses = await api.getClassesForSchool(schoolId, userId); if (fetchedClasses.length === 0) { - history.replace(PAGES.ADD_CLASS, { - school: { id: schoolId }, - origin: originPage, + history.replace({ + ...parsePath(PAGES.ADD_CLASS), + state: { + school: { id: schoolId }, + origin: originPage, + }, }); return; } From 3e860328741e6d09aae995142bc1b7976cc5b452 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Fri, 10 Jul 2026 11:53:35 +0530 Subject: [PATCH 3/7] refactor: app init, GrowthBook hook & popup nav Move app routing/content into AppContent and simplify App bootstrap. Switch from the legacy GrowthBook API to useFeatureValue across pages/hooks to read the GENERIC_POP_UP feature. Add URL building for hash vs non-hash routers and update replaceLocation to respect app routing. Fix GenericPopUp behavior (isPopupActive lifecycle) and broaden PopupConfig typing to include JSONValue. Clean up DataClear and Sidebar localStorage handling. Update tests and mocks to match router and GrowthBook changes. Files changed: App.tsx, app/AppContent.tsx, hooks/useGenericPopup.ts, pages/*, components/GenericPopUp/*, navigation utilities, tests and mocks. Why: reduces bootstrap complexity, unifies feature-flag usage, fixes popup navigation issues, and aligns tests with hash routing. --- src/App.test.tsx | 41 ++++++-------- src/App.tsx | 49 ++--------------- src/app/AppContent.tsx | 53 +++++++++++++++++++ .../GenericPopUp/GenericPopUpManager.tsx | 4 +- .../GenericPopUp/GenericPopUpType.tsx | 8 ++- .../GenericPopUp/navigation.test.ts | 23 ++++++++ src/components/GenericPopUp/navigation.ts | 10 +++- src/components/parent/DataClear.tsx | 2 +- src/hooks/useGenericPopup.ts | 12 ++--- src/ops-console/components/Sidebar.tsx | 8 +-- src/pages/Home.test.tsx | 33 ++++++------ src/pages/Home.tsx | 12 ++--- src/pages/LidoPlayer.tsx | 8 +-- src/pages/LiveQuizGame.test.tsx | 47 ++++++++++------ src/pages/LiveQuizGame.tsx | 8 +-- src/startup/renderRoot.tsx | 14 ++--- src/tests/__mocks__/@ionic/react-router.tsx | 22 ++++++-- 17 files changed, 202 insertions(+), 152 deletions(-) create mode 100644 src/app/AppContent.tsx create mode 100644 src/components/GenericPopUp/navigation.test.ts diff --git a/src/App.test.tsx b/src/App.test.tsx index 5a8f9be5ca..f922ed67c3 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,11 +1,11 @@ // Coverage: App-level GrowthBook popup routing by URL tab/screen, trigger payload variants, false-positive mismatch blocking, and malformed/null payload negatives. import { waitFor } from '@testing-library/react'; import { act } from '@testing-library/react'; -import { BrowserRouter } from 'react-router-dom'; import { renderWithProviders } from './tests/test-utils'; import App from './App'; import PopupManager from './components/GenericPopUp/GenericPopUpManager'; import * as growthbookModule from '@growthbook/growthbook-react'; +import { GENERIC_POP_UP } from './common/constants'; jest.mock('@growthbook/growthbook-react', () => ({ GrowthBookProvider: ({ children }: any) => children, @@ -50,11 +50,7 @@ describe('App Component', () => { act(() => { mockGrowthbook.getFeatureValue.mockReturnValue(null); - const result = renderWithProviders( - - - , - ); + const result = renderWithProviders(); unmount = result.unmount; }); @@ -117,14 +113,13 @@ describe('App Component', () => { triggers: trigger, }; - mockGrowthbook.getFeatureValue.mockReturnValue(popupConfig); + (growthbookModule.useFeatureValue as jest.Mock).mockImplementation( + (key, defaultValue) => + key === GENERIC_POP_UP ? popupConfig : defaultValue, + ); window.history.replaceState({}, '', `/?tab=${tab}`); - renderWithProviders( - - - , - ); + renderWithProviders(); await waitFor(() => expect(PopupManager.onAppOpen).toHaveBeenCalledWith(popupConfig), @@ -144,14 +139,13 @@ describe('App Component', () => { triggers: { type: 'APP_OPEN', value: 1 }, }; - mockGrowthbook.getFeatureValue.mockReturnValue(popupConfig); + (growthbookModule.useFeatureValue as jest.Mock).mockImplementation( + (key, defaultValue) => + key === GENERIC_POP_UP ? popupConfig : defaultValue, + ); window.history.replaceState({}, '', '/?tab=home'); - renderWithProviders( - - - , - ); + renderWithProviders(); await new Promise((resolve) => setTimeout(resolve, 50)); expect(PopupManager.onAppOpen).not.toHaveBeenCalled(); @@ -165,15 +159,14 @@ describe('App Component', () => { ])( 'does not route popup when growthbook payload is malformed: $name', async ({ payload }) => { - mockGrowthbook.getFeatureValue.mockReturnValue(payload); + (growthbookModule.useFeatureValue as jest.Mock).mockImplementation( + (key, defaultValue) => + key === GENERIC_POP_UP ? payload : defaultValue, + ); window.history.replaceState({}, '', '/?tab=leaderboard'); - renderWithProviders( - - - , - ); + renderWithProviders(); await new Promise((resolve) => setTimeout(resolve, 50)); expect(PopupManager.onAppOpen).not.toHaveBeenCalled(); diff --git a/src/App.tsx b/src/App.tsx index ead281ba65..a7a37932e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; +import { IonApp, setupIonicReact } from '@ionic/react'; import { IonReactHashRouter } from '@ionic/react-router'; /* Core CSS required for Ionic components to work properly */ @@ -39,36 +39,18 @@ import './theme/variables.css'; import './App.css'; import React from 'react'; -import TermsGate from './components/termsandconditons/TermsGate'; -import { HardwareBackButtonHandler } from './common/backButtonRegistry'; -import { useAppSelector } from './redux/hooks'; -import { useNavigationHandler } from './helper/navigation/NavigationHandler'; -import AppRoutes from './app/AppRoutes'; -import AppOverlays from './app/AppOverlays'; -import { useGenericPopup } from './hooks/useGenericPopup'; + import { useGlobalBrowserEffects } from './hooks/useGlobalBrowserEffects'; import { useGrowthBookFeatureCache } from './hooks/useGrowthBookFeatureCache'; import { useHotUpdate } from './hooks/useHotUpdate'; import { useNativeAppListeners } from './hooks/useNativeAppListeners'; -import { useOpsConsoleBodyClass } from './hooks/useOpsConsoleBodyClass'; import { useRemoteAssetFlags } from './hooks/useRemoteAssetFlags'; -import { useRouteAudioCleanup } from './hooks/useRouteAudioCleanup'; -import { useUsageLimitModal } from './hooks/useUsageLimitModal'; -setupIonicReact(); +import AppContent from './app/AppContent'; -const AppRouteEffects = () => { - useNavigationHandler(); - useOpsConsoleBodyClass(); - useRouteAudioCleanup(); - return null; -}; +setupIonicReact(); const App: React.FC = () => { - const isGlobalLoading = useAppSelector((state) => state.auth.globalLoading); - const popup = useGenericPopup(); - const usageLimit = useUsageLimitModal(); - useGrowthBookFeatureCache(); useRemoteAssetFlags(); useGlobalBrowserEffects(); @@ -78,28 +60,7 @@ const App: React.FC = () => { return ( - - - - - - - usageLimit.setShowToast(false)} - /> + ); diff --git a/src/app/AppContent.tsx b/src/app/AppContent.tsx new file mode 100644 index 0000000000..92e1c55a16 --- /dev/null +++ b/src/app/AppContent.tsx @@ -0,0 +1,53 @@ +import { IonRouterOutlet } from '@ionic/react'; +import { HardwareBackButtonHandler } from '../common/backButtonRegistry'; +import TermsGate from '../components/termsandconditons/TermsGate'; +import { useNavigationHandler } from '../helper/navigation/NavigationHandler'; +import { useGenericPopup } from '../hooks/useGenericPopup'; +import { useOpsConsoleBodyClass } from '../hooks/useOpsConsoleBodyClass'; +import { useRouteAudioCleanup } from '../hooks/useRouteAudioCleanup'; +import { useUsageLimitModal } from '../hooks/useUsageLimitModal'; +import { useAppSelector } from '../redux/hooks'; +import AppRoutes from './AppRoutes'; +import AppOverlays from './AppOverlays'; + +const AppRouteEffects = () => { + useNavigationHandler(); + useOpsConsoleBodyClass(); + useRouteAudioCleanup(); + return null; +}; + +const AppContent: React.FC = () => { + const isGlobalLoading = useAppSelector((state) => state.auth.globalLoading); + const popup = useGenericPopup(); + const usageLimit = useUsageLimitModal(); + + return ( + <> + + + + + + + usageLimit.setShowToast(false)} + /> + + ); +}; + +export default AppContent; diff --git a/src/components/GenericPopUp/GenericPopUpManager.tsx b/src/components/GenericPopUp/GenericPopUpManager.tsx index 8489815c89..029119fde2 100644 --- a/src/components/GenericPopUp/GenericPopUpManager.tsx +++ b/src/components/GenericPopUp/GenericPopUpManager.tsx @@ -151,6 +151,8 @@ class PopupManager { target: config.action?.target, }); + this.isPopupActive = false; + if (config.action?.type === 'DEEP_LINK') { const rawTarget = config.action.target; // e.g. "SUBJECTS", "LEADERBOARD" const normalizedTarget = rawTarget.toLowerCase(); @@ -178,8 +180,6 @@ class PopupManager { window.open(rawTarget, '_blank', 'noopener,noreferrer'); } } - - this.isPopupActive = false; } } diff --git a/src/components/GenericPopUp/GenericPopUpType.tsx b/src/components/GenericPopUp/GenericPopUpType.tsx index 4b14e2c62b..036aa34aff 100644 --- a/src/components/GenericPopUp/GenericPopUpType.tsx +++ b/src/components/GenericPopUp/GenericPopUpType.tsx @@ -1,6 +1,8 @@ +import type { JSONValue } from '@growthbook/growthbook-react'; + export type TriggerType = 'APP_OPEN' | 'GAME_COMPLETE' | 'TIME_ELAPSED'; -export interface PopupConfig { +export type PopupConfig = { id: string; isActive: boolean; priority: number; @@ -38,4 +40,6 @@ export interface PopupConfig { type: 'DEEP_LINK'; target: string; }; -} +} & { + [key: string]: JSONValue; +}; diff --git a/src/components/GenericPopUp/navigation.test.ts b/src/components/GenericPopUp/navigation.test.ts new file mode 100644 index 0000000000..0960dd9c59 --- /dev/null +++ b/src/components/GenericPopUp/navigation.test.ts @@ -0,0 +1,23 @@ +import { buildReplacementLocation } from './navigation'; + +describe('GenericPopUp navigation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('builds hash-router urls for internal popup targets when the app is hash-routed', () => { + window.history.replaceState({}, '', 'http://localhost/#/leaderboard'); + + expect(buildReplacementLocation('/home?tab=SUBJECTS')).toBe( + 'http://localhost/#/home?tab=SUBJECTS', + ); + }); + + it('builds pathname urls for internal popup targets when the app is not hash-routed', () => { + window.history.replaceState({}, '', 'http://localhost/leaderboard'); + + expect(buildReplacementLocation('/home?tab=SUBJECTS')).toBe( + 'http://localhost/home?tab=SUBJECTS', + ); + }); +}); diff --git a/src/components/GenericPopUp/navigation.ts b/src/components/GenericPopUp/navigation.ts index f32d2c4a26..07ab952928 100644 --- a/src/components/GenericPopUp/navigation.ts +++ b/src/components/GenericPopUp/navigation.ts @@ -1,3 +1,11 @@ +import { parsePath } from 'history'; + +import { buildAppUrl } from '../../utility/routerLocation'; + +export const buildReplacementLocation = (url: string) => + buildAppUrl(parsePath(url)).toString(); + export const replaceLocation = (url: string) => { - window.location.replace(url); + const target = buildReplacementLocation(url); + window.location.replace(target); }; diff --git a/src/components/parent/DataClear.tsx b/src/components/parent/DataClear.tsx index 3c7a1ad6b2..d9377135d2 100644 --- a/src/components/parent/DataClear.tsx +++ b/src/components/parent/DataClear.tsx @@ -18,7 +18,7 @@ const KEYS_TO_CLEAR = [ CURRENT_STUDENT, ] as const; -export const KEYS_TO_PRESERVE = new Set([ +const KEYS_TO_PRESERVE = new Set([ BUNDLED_IMPORT_APP_VERSION_KEY, CURRENT_SQLITE_VERSION, ]); diff --git a/src/hooks/useGenericPopup.ts b/src/hooks/useGenericPopup.ts index 5f9955c454..bbbe6f7d95 100644 --- a/src/hooks/useGenericPopup.ts +++ b/src/hooks/useGenericPopup.ts @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { useGrowthBook } from '@growthbook/growthbook-react'; +import { useFeatureValue } from '@growthbook/growthbook-react'; import { GENERIC_POP_UP, SHOW_GENERIC_POPUP } from '../common/constants'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; import { PopupConfig } from '../components/GenericPopUp/GenericPopUpType'; @@ -14,8 +14,8 @@ type PopupEventDetail = { }; export const useGenericPopup = () => { - const growthbook = useGrowthBook(); const location = useLocation(); + const popupConfig = useFeatureValue(GENERIC_POP_UP, null); const [popupData, setPopupData] = useState(null); const popupDataRef = useRef(null); @@ -37,12 +37,6 @@ export const useGenericPopup = () => { }, []); useEffect(() => { - if (!growthbook) return; - - const popupConfig = growthbook.getFeatureValue( - GENERIC_POP_UP, - null, - ); if (!popupConfig) return; const params = new URLSearchParams(location.search); @@ -56,7 +50,7 @@ export const useGenericPopup = () => { PopupManager.onAppOpen(popupConfig); PopupManager.onTimeElapsed(popupConfig); } - }, [growthbook, location.search]); + }, [location.search, popupConfig]); const closePopup = () => { if (!popupData) return; diff --git a/src/ops-console/components/Sidebar.tsx b/src/ops-console/components/Sidebar.tsx index 9435656ad3..21916abb11 100644 --- a/src/ops-console/components/Sidebar.tsx +++ b/src/ops-console/components/Sidebar.tsx @@ -35,7 +35,6 @@ import { RootState } from '../../redux/store'; import { AuthState } from '../../redux/slices/auth/authSlice'; import { logAuthDebug } from '../../utility/authDebug'; import { getAppPathname } from '../../utility/routerLocation'; -import { KEYS_TO_PRESERVE } from '../../components/parent/DataClear'; import { parsePath } from 'history'; interface SidebarProps { @@ -178,12 +177,7 @@ const Sidebar: React.FC = ({ name, email, photo }) => { }); await auth.logOut(); Util.unSubscribeToClassTopicForAllStudents(); - for (let i = localStorage.length - 1; i >= 0; i--) { - const key = localStorage.key(i); - if (key && !KEYS_TO_PRESERVE.has(key)) { - localStorage.removeItem(key); - } - } + localStorage.clear(); const serviceInstance = ServiceConfig.getInstance(APIMode.SQLITE); serviceInstance.switchMode(APIMode.SQLITE); logAuthDebug('Navigating to login after ops console logout.', { diff --git a/src/pages/Home.test.tsx b/src/pages/Home.test.tsx index ed05b54cab..b9afb1144d 100644 --- a/src/pages/Home.test.tsx +++ b/src/pages/Home.test.tsx @@ -18,7 +18,8 @@ import { } from '../common/constants'; import { updateLocalAttributes } from '../growthbook/Growthbook'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; -import { useFeatureIsOn, useGrowthBook } from '@growthbook/growthbook-react'; +import { GENERIC_POP_UP } from '../common/constants'; +import { useFeatureIsOn, useFeatureValue } from '@growthbook/growthbook-react'; const mockHistoryReplace = jest.fn(); const mockHistoryPush = jest.fn(); @@ -97,9 +98,7 @@ jest.mock('../growthbook/Growthbook', () => ({ })); jest.mock('@growthbook/growthbook-react', () => ({ useFeatureIsOn: jest.fn(() => false), - useGrowthBook: jest.fn(() => ({ - getFeatureValue: jest.fn(() => null), - })), + useFeatureValue: jest.fn((_, defaultValue) => defaultValue), })); jest.mock('@capacitor/app', () => ({ App: { addListener: jest.fn() }, @@ -162,8 +161,8 @@ describe('Home page (Home tab)', () => { mockApi.assignmentListner.mockResolvedValue(undefined); mockApi.assignmentUserListner.mockResolvedValue(undefined); (useFeatureIsOn as jest.Mock).mockReturnValue(false); - (useGrowthBook as jest.Mock).mockReturnValue({ - getFeatureValue: jest.fn(() => null), + (useFeatureValue as jest.Mock).mockImplementation((_, defaultValue) => { + return defaultValue; }); window.history.replaceState({}, '', '/'); }); @@ -715,9 +714,9 @@ describe('Home page (Home tab)', () => { test.each(fullPopupConfigVariations)( 'triggers generic popup handlers for fullPopupConfig variation: $name', async ({ config }) => { - (useGrowthBook as jest.Mock).mockReturnValue({ - getFeatureValue: jest.fn(() => config), - }); + (useFeatureValue as jest.Mock).mockImplementation((key, defaultValue) => + key === GENERIC_POP_UP ? config : defaultValue, + ); render(); @@ -733,8 +732,8 @@ describe('Home page (Home tab)', () => { // Covers: does NOT trigger generic popup handlers when growthbook returns null test('does NOT trigger generic popup handlers when growthbook returns null', async () => { - (useGrowthBook as jest.Mock).mockReturnValue({ - getFeatureValue: jest.fn(() => null), + (useFeatureValue as jest.Mock).mockImplementation((_, defaultValue) => { + return defaultValue; }); render(); @@ -767,9 +766,9 @@ describe('Home page (Home tab)', () => { ])( 'false-positive prevention: $name', async ({ config, clickButtonText }) => { - (useGrowthBook as jest.Mock).mockReturnValue({ - getFeatureValue: jest.fn(() => config), - }); + (useFeatureValue as jest.Mock).mockImplementation((key, defaultValue) => + key === GENERIC_POP_UP ? config : defaultValue, + ); render(); @@ -805,9 +804,9 @@ describe('Home page (Home tab)', () => { ])( 'negative payload: does NOT trigger popup handlers when growthbook payload is $name', async ({ config }) => { - (useGrowthBook as jest.Mock).mockReturnValue({ - getFeatureValue: jest.fn(() => config), - }); + (useFeatureValue as jest.Mock).mockImplementation((key, defaultValue) => + key === GENERIC_POP_UP ? config : defaultValue, + ); render(); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 306acdd0d1..ea84aead93 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -39,10 +39,9 @@ import { AvatarObj } from '../components/animation/Avatar'; import LearningPathway from '../components/LearningPathway'; import { updateLocalAttributes, useGbContext } from '../growthbook/Growthbook'; import i18n from '../i18n'; -import { useFeatureIsOn } from '@growthbook/growthbook-react'; +import { useFeatureIsOn, useFeatureValue } from '@growthbook/growthbook-react'; import WinterCampaignPopupGating from '../components/WinterCampaignPopup/WinterCampaignPopupGating'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; -import { useGrowthBook } from '@growthbook/growthbook-react'; import ActivationLessonBanner from '../components/activationLesson/ActivationLessonBanner'; import logger from '../utility/logger'; import { @@ -51,6 +50,7 @@ import { } from '../components/assignment/homeworkPathwayHelpers'; import { replaceWithNavigationTarget } from '../helper/navigation/NavigationHandler'; import { getAppSearchParams } from '../utility/routerLocation'; +import { PopupConfig } from '../components/GenericPopUp/GenericPopUpType'; const localData: any = {}; @@ -134,12 +134,8 @@ const Home: FC = () => { } }, [currentHeader, history, location.pathname, location.search]); - const growthbook = useGrowthBook(); + const popupConfig = useFeatureValue(GENERIC_POP_UP, null); useEffect(() => { - if (!growthbook) return; - - const popupConfig = growthbook.getFeatureValue(GENERIC_POP_UP, null) as any; - if (!popupConfig) return; if ( @@ -150,7 +146,7 @@ const Home: FC = () => { PopupManager.onAppOpen(popupConfig); PopupManager.onTimeElapsed(popupConfig); } - }, [growthbook, currentHeader]); + }, [currentHeader, popupConfig]); useEffect(() => { const student = Util.getCurrentStudent(); diff --git a/src/pages/LidoPlayer.tsx b/src/pages/LidoPlayer.tsx index 8b900ada75..95daff0691 100644 --- a/src/pages/LidoPlayer.tsx +++ b/src/pages/LidoPlayer.tsx @@ -24,6 +24,7 @@ import { RESULT_STATUS, SOURCE, CURRENT_HEADER, + GENERIC_POP_UP, } from '../common/constants'; import Loading from '../components/Loading'; import ScoreCard from '../components/scorecards/ScoreCard'; @@ -39,7 +40,7 @@ import React from 'react'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { palUtil } from '../utility/palUtil'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; -import { useGrowthBook } from '@growthbook/growthbook-react'; +import { useFeatureValue } from '@growthbook/growthbook-react'; import { registerBackButtonHandler } from '../common/backButtonRegistry'; import logger from '../utility/logger'; import { getCachedGrowthBookFeatureValue } from '../growthbook/Growthbook'; @@ -49,6 +50,7 @@ import { } from '../services/RemoteConfig'; import { getAppPathname, getAppSearchParams } from '../utility/routerLocation'; import { parsePath } from 'history'; +import { PopupConfig } from '../components/GenericPopUp/GenericPopUpType'; const HOMEWORK_REWARD_COMPLETED_INDEX_KEY = 'homework_reward_completed_index'; const PENDING_HOMEWORK_REWARD_TRANSITION_KEY = @@ -155,7 +157,7 @@ const LidoPlayer: FC = () => { }); const [isReady, setIsReady] = useState(false); const [gameResult, setGameResult] = useState(null); - const growthbook = useGrowthBook(); + const popupConfig = useFeatureValue(GENERIC_POP_UP, null); // Data Objects // Ensure we handle String vs string here if needed, but usually these are safe if parsed from JSON @@ -512,8 +514,6 @@ const LidoPlayer: FC = () => { const onNextContainer = (e: any) => logger.info('Next', e); const gameCompleted = () => { - const popupConfig = growthbook?.getFeatureValue('generic-pop-up', null); - if (popupConfig) { PopupManager.onGameComplete(popupConfig); } diff --git a/src/pages/LiveQuizGame.test.tsx b/src/pages/LiveQuizGame.test.tsx index 04994ce1d7..a5cbcc5a7b 100644 --- a/src/pages/LiveQuizGame.test.tsx +++ b/src/pages/LiveQuizGame.test.tsx @@ -36,9 +36,7 @@ jest.mock('../common/onlineOfflineErrorMessageHandler', () => ({ })); jest.mock('@growthbook/growthbook-react', () => ({ - useGrowthBook: () => ({ - getFeatureValue: jest.fn(() => ({ enabled: true })), - }), + useFeatureValue: () => ({ enabled: true }), })); jest.mock('../components/GenericPopUp/GenericPopUpManager', () => ({ @@ -372,9 +370,14 @@ describe('LiveQuizGame page', () => { fireEvent.click(await screen.findByText('trigger-quiz-end')); fireEvent.click(await screen.findByText('continue')); await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/home', { - ...mockState, - fromLiveQuiz: true, + expect(mockReplace).toHaveBeenCalledWith({ + pathname: '/home', + search: '', + hash: '', + state: { + ...mockState, + fromLiveQuiz: true, + }, }); }); }); @@ -394,13 +397,15 @@ describe('LiveQuizGame page', () => { fireEvent.click(await screen.findByText('trigger-quiz-end')); fireEvent.click(await screen.findByText('continue')); await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith( - '/home?tab=ASSIGNMENT&isReload=true', - { + expect(mockReplace).toHaveBeenCalledWith({ + pathname: '/home', + search: '?tab=ASSIGNMENT&isReload=true', + hash: '', + state: { ...mockState, fromLiveQuiz: true, }, - ); + }); }); }); @@ -419,9 +424,14 @@ describe('LiveQuizGame page', () => { fireEvent.click(await screen.findByText('trigger-quiz-end')); fireEvent.click(await screen.findByText('continue')); await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/home?isReload=true', { - ...mockState, - fromLiveQuiz: true, + expect(mockReplace).toHaveBeenCalledWith({ + pathname: '/home', + search: '?isReload=true', + hash: '', + state: { + ...mockState, + fromLiveQuiz: true, + }, }); }); }); @@ -440,9 +450,14 @@ describe('LiveQuizGame page', () => { fireEvent.click(await screen.findByText('trigger-quiz-end')); fireEvent.click(await screen.findByText('continue')); await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith(PAGES.HOME, { - ...mockState, - fromLiveQuiz: true, + expect(mockReplace).toHaveBeenCalledWith({ + pathname: PAGES.HOME, + search: '', + hash: '', + state: { + ...mockState, + fromLiveQuiz: true, + }, }); }); }); diff --git a/src/pages/LiveQuizGame.tsx b/src/pages/LiveQuizGame.tsx index 79c1be8231..4cd16f774d 100644 --- a/src/pages/LiveQuizGame.tsx +++ b/src/pages/LiveQuizGame.tsx @@ -3,6 +3,7 @@ import { FC, useEffect, useState } from 'react'; import { ServiceConfig } from '../services/ServiceConfig'; import { useHistory } from 'react-router'; import { + GENERIC_POP_UP, IS_REWARD_FEATURE_ON, LESSONS_PLAYED_COUNT, PAGES, @@ -20,9 +21,10 @@ import { Util } from '../utility/util'; import { t } from 'i18next'; import { Capacitor } from '@capacitor/core'; import PopupManager from '../components/GenericPopUp/GenericPopUpManager'; -import { useGrowthBook } from '@growthbook/growthbook-react'; +import { useFeatureValue } from '@growthbook/growthbook-react'; import { getAppSearchParams } from '../utility/routerLocation'; import { parsePath } from 'history'; +import { PopupConfig } from '../components/GenericPopUp/GenericPopUpType'; const LiveQuizGame: FC = () => { const api = ServiceConfig.getI().apiHandler; @@ -62,7 +64,7 @@ const LiveQuizGame: FC = () => { SOURCE.LEARNING_PATHWAY_HOME_PAL, SOURCE.INITIAL_ASSESSMENT, ].includes(source); - const growthbook = useGrowthBook(); + const popupConfig = useFeatureValue(GENERIC_POP_UP, null); useEffect(() => { if (!paramLiveRoomId && !paramLessonId) { @@ -125,8 +127,6 @@ const LiveQuizGame: FC = () => { setShowScoreCard(true); setShowDialogBox(true); - const popupConfig = growthbook?.getFeatureValue('generic-pop-up', null); - if (popupConfig) { PopupManager.onGameComplete(popupConfig); } diff --git a/src/startup/renderRoot.tsx b/src/startup/renderRoot.tsx index 48365b5f9e..4b179c28a7 100644 --- a/src/startup/renderRoot.tsx +++ b/src/startup/renderRoot.tsx @@ -1,6 +1,4 @@ -import React from 'react'; import { createRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; @@ -23,13 +21,11 @@ export const renderRoot = ( root.render( } persistor={persistor}> - - - - - - - + + + + + , ); diff --git a/src/tests/__mocks__/@ionic/react-router.tsx b/src/tests/__mocks__/@ionic/react-router.tsx index 7e47f9c755..1d45e7c608 100644 --- a/src/tests/__mocks__/@ionic/react-router.tsx +++ b/src/tests/__mocks__/@ionic/react-router.tsx @@ -1,8 +1,22 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -export const IonReactRouter: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - return {children}; +const getInitialEntry = () => { + if (typeof window === 'undefined') { + return '/'; + } + + if (window.location.hash.startsWith('#/')) { + return window.location.hash.slice(1); + } + + return `${window.location.pathname}${window.location.search}${window.location.hash}`; }; + +const MockIonicRouter: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( + {children} +); + +export const IonReactHashRouter = MockIonicRouter; From e33336917222ea56e3b950054b0293c931a88697 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Fri, 10 Jul 2026 15:38:07 +0530 Subject: [PATCH 4/7] test: fix broken tests from HashRouter migration --- .codex | 0 .../GenericPopUp/GenericPopUpManager.test.ts | 87 +++++++++++++++---- .../ProfileMenu/ProfileMenu.test.tsx | 6 +- .../StickerBookPreviewModal.logic.test.tsx | 17 ++-- .../StickerBookPreviewModal.test.tsx | 17 ++-- src/components/parent/ProfileCard.test.tsx | 13 ++- src/pages/StickerBook.test.tsx | 17 ++-- .../assignment/QRAssignments.test.tsx | 38 ++++---- .../TeacherRecommendedAssignments.test.tsx | 8 +- 9 files changed, 144 insertions(+), 59 deletions(-) delete mode 100644 .codex diff --git a/.codex b/.codex deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/GenericPopUp/GenericPopUpManager.test.ts b/src/components/GenericPopUp/GenericPopUpManager.test.ts index 76413e6f39..e8d983f0c0 100644 --- a/src/components/GenericPopUp/GenericPopUpManager.test.ts +++ b/src/components/GenericPopUp/GenericPopUpManager.test.ts @@ -49,7 +49,8 @@ describe('GenericPopUpManager', () => { mockedGetCurrentStudent.mockReturnValue({ id: 'student-1' }); installAnalyticsMock(); delete (window as any).isAnyPopupOpen; - (PopupManager as any).isPopupActive = false; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + false; (PopupManager as any).sessionGamesPlayed = 0; }); @@ -220,7 +221,8 @@ describe('GenericPopUpManager', () => { const config = createPopupConfig(); const { handler, cleanup } = listenForPopupEvent(); - (PopupManager as any).isPopupActive = true; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + true; PopupManager.onAppOpen(config); expect(handler).not.toHaveBeenCalled(); @@ -395,7 +397,8 @@ describe('GenericPopUpManager', () => { expect(eventEs.detail.localized.heading).toBe('Encabezado'); handler.mockClear(); - (PopupManager as any).isPopupActive = false; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + false; localStorage.setItem(LANGUAGE, 'fr'); PopupManager.onAppOpen(config); @@ -433,25 +436,29 @@ describe('GenericPopUpManager', () => { it('tracks dismiss analytics and clears active state', () => { const config = createPopupConfig(); const track = (window as any).analytics.track as jest.Mock; - (PopupManager as any).isPopupActive = true; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + true; PopupManager.onDismiss(config); - expect((PopupManager as any).isPopupActive).toBe(false); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); expect(track).toHaveBeenCalledWith('popup_dismissed', { popup_id: config.id, }); }); - // Covers: routes deep-link tab targets to internal home tabs and keeps active flag (current behavior) + // Covers: routes deep-link tab targets to internal home tabs and clears active flag - it('routes deep-link tab targets to internal home tabs and keeps active flag (current behavior)', () => { + it('routes deep-link tab targets to internal home tabs and clears active flag', () => { const config = createPopupConfig({ action: { type: 'DEEP_LINK', target: 'SUBJECTS' }, }); const { replaceSpy, restore } = mockNavigation(); const track = (window as any).analytics.track as jest.Mock; - (PopupManager as any).isPopupActive = true; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + true; PopupManager.onAction(config); @@ -461,7 +468,9 @@ describe('GenericPopUpManager', () => { target: 'SUBJECTS', }); expect(replaceSpy).toHaveBeenCalledWith('/home?tab=SUBJECTS'); - expect((PopupManager as any).isPopupActive).toBe(true); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); restore(); }); @@ -490,6 +499,35 @@ describe('GenericPopUpManager', () => { PopupManager.onAction(config); expect(replaceSpy).toHaveBeenCalledWith('/profile'); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); + restore(); + }); + + // Covers: internal CTA navigation does not leave popup manager locked for later popups + + it('allows later popups after internal CTA navigation', () => { + const config = createPopupConfig({ + action: { type: 'DEEP_LINK', target: 'SUBJECTS' }, + }); + const { replaceSpy, restore } = mockNavigation(); + const { handler, cleanup } = listenForPopupEvent(); + + PopupManager.onAppOpen(config); + expect(handler).toHaveBeenCalledTimes(1); + + PopupManager.onAction(config); + expect(replaceSpy).toHaveBeenCalledWith('/home?tab=SUBJECTS'); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); + + handler.mockClear(); + PopupManager.onAppOpen(config); + expect(handler).toHaveBeenCalledTimes(1); + + cleanup(); restore(); }); @@ -501,7 +539,8 @@ describe('GenericPopUpManager', () => { }); const { openSpy, restore } = mockNavigation(); (Capacitor.isNativePlatform as jest.Mock).mockReturnValue(false); - (PopupManager as any).isPopupActive = true; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + true; PopupManager.onAction(config); @@ -510,7 +549,9 @@ describe('GenericPopUpManager', () => { '_blank', 'noopener,noreferrer', ); - expect((PopupManager as any).isPopupActive).toBe(false); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); restore(); }); @@ -533,11 +574,14 @@ describe('GenericPopUpManager', () => { it('clears active state when action is missing', () => { const config = createPopupConfig({ action: undefined }); - (PopupManager as any).isPopupActive = true; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + true; PopupManager.onAction(config); - expect((PopupManager as any).isPopupActive).toBe(false); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); }); // Covers: does not crash when analytics object is missing @@ -708,7 +752,9 @@ describe('GenericPopUpManager', () => { action_type: 'CLICK_BUTTON', target: 'something', }); - expect((PopupManager as any).isPopupActive).toBe(false); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); }); // Covers: onDismiss is idempotent when already inactive @@ -716,11 +762,14 @@ describe('GenericPopUpManager', () => { it('onDismiss is idempotent when already inactive', () => { const config = createPopupConfig(); const track = (window as any).analytics.track as jest.Mock; - (PopupManager as any).isPopupActive = false; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + false; PopupManager.onDismiss(config); - expect((PopupManager as any).isPopupActive).toBe(false); + expect( + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive, + ).toBe(false); expect(track).toHaveBeenCalledTimes(1); expect(track).toHaveBeenCalledWith('popup_dismissed', { popup_id: config.id, @@ -736,7 +785,8 @@ describe('GenericPopUpManager', () => { value: 1, }, }); - (PopupManager as any).isPopupActive = true; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + true; const { handler, cleanup } = listenForPopupEvent(); PopupManager.onTimeElapsed(config); @@ -762,7 +812,8 @@ describe('GenericPopUpManager', () => { expect(handler).toHaveBeenCalledTimes(1); handler.mockClear(); - (PopupManager as any).isPopupActive = false; + (PopupManager as unknown as { isPopupActive: boolean }).isPopupActive = + false; PopupManager.onGameComplete(config); PopupManager.onGameComplete(config); diff --git a/src/components/ProfileMenu/ProfileMenu.test.tsx b/src/components/ProfileMenu/ProfileMenu.test.tsx index e1a6c72aac..4aa8a9cfb7 100644 --- a/src/components/ProfileMenu/ProfileMenu.test.tsx +++ b/src/components/ProfileMenu/ProfileMenu.test.tsx @@ -199,8 +199,10 @@ describe('ProfileMenu Notification Logic', () => { }); expect(mockPush).toHaveBeenCalledWith( - PAGES.STICKER_BOOK, - expect.any(Object), + expect.objectContaining({ + pathname: PAGES.STICKER_BOOK, + state: expect.any(Object), + }), ); }); diff --git a/src/components/learningPathway/StickerBookPreviewModal.logic.test.tsx b/src/components/learningPathway/StickerBookPreviewModal.logic.test.tsx index 7fbce947e6..19ebe5128a 100644 --- a/src/components/learningPathway/StickerBookPreviewModal.logic.test.tsx +++ b/src/components/learningPathway/StickerBookPreviewModal.logic.test.tsx @@ -418,12 +418,17 @@ describe('useStickerBookPreviewModalLogic', () => { }), ); expect(onClose).toHaveBeenCalledWith('acknowledge_button'); - expect(mockPush).toHaveBeenCalledWith(PAGES.COLORING_BOARD, { - stickerBookId: 'book-1', - svgRaw: expect.stringContaining(' { }), ); expect(onClose).toHaveBeenCalledWith('acknowledge_button'); - expect(mockPush).toHaveBeenCalledWith(PAGES.COLORING_BOARD, { - stickerBookId: 'book-complete', - svgRaw: expect.stringContaining(' { {}, ), ); - expect(mockHistoryPush).toHaveBeenCalledWith(PAGES.CREATE_STUDENT, { - isEdit: false, - from: '/parent', - }); + expect(mockHistoryPush).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: PAGES.CREATE_STUDENT, + state: { + isEdit: false, + from: '/parent', + }, + }), + ); }); }); diff --git a/src/pages/StickerBook.test.tsx b/src/pages/StickerBook.test.tsx index d485ced2f8..aec7bddd99 100644 --- a/src/pages/StickerBook.test.tsx +++ b/src/pages/StickerBook.test.tsx @@ -472,12 +472,17 @@ describe('StickerBook page', () => { props.onPaint(); }); - expect(pushMock).toHaveBeenCalledWith(PAGES.COLORING_BOARD, { - stickerBookId: 'book-7', - svgRaw: '', - svgUrl: '/assets/books/book-1.svg', - artworkTitle: 'Animals', - returnTo: PAGES.STICKER_BOOK, + expect(pushMock).toHaveBeenCalledWith({ + pathname: PAGES.COLORING_BOARD, + search: '', + hash: '', + state: { + stickerBookId: 'book-7', + svgRaw: '', + svgUrl: '/assets/books/book-1.svg', + artworkTitle: 'Animals', + returnTo: PAGES.STICKER_BOOK, + }, }); }); diff --git a/src/teachers-module/components/homePage/assignment/QRAssignments.test.tsx b/src/teachers-module/components/homePage/assignment/QRAssignments.test.tsx index f75aab0f33..12c208fb29 100644 --- a/src/teachers-module/components/homePage/assignment/QRAssignments.test.tsx +++ b/src/teachers-module/components/homePage/assignment/QRAssignments.test.tsx @@ -210,15 +210,17 @@ describe('QRAssignments – full coverage', () => { await userEvent.click(button); expect(historyPush).toHaveBeenCalledWith( - PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, expect.objectContaining({ - fromPage: PAGES.QR_ASSIGNMENTS, - selectedAssignments: expect.any(Object), - manualAssignments: expect.any(Object), - recommendedAssignments: {}, - qrAssignmentNavigationState: expect.objectContaining({ - chapterId: 'chapter-1', - courseId: 'course-1', + pathname: PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, + state: expect.objectContaining({ + fromPage: PAGES.QR_ASSIGNMENTS, + selectedAssignments: expect.any(Object), + manualAssignments: expect.any(Object), + recommendedAssignments: {}, + qrAssignmentNavigationState: expect.objectContaining({ + chapterId: 'chapter-1', + courseId: 'course-1', + }), }), }), ); @@ -247,9 +249,12 @@ describe('QRAssignments – full coverage', () => { await screen.findByText('Lesson 2'); await userEvent.click(screen.getByRole('button', { name: 'Header' })); - expect(historyReplace).toHaveBeenCalledWith(PAGES.HOME_PAGE, { - tabValue: 2, - }); + expect(historyReplace).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: PAGES.HOME_PAGE, + state: { tabValue: 2 }, + }), + ); }); test('back uses history.goBack when not opened from home page', async () => { @@ -273,9 +278,12 @@ describe('QRAssignments – full coverage', () => { await userEvent.click(screen.getByRole('button', { name: 'Header' })); expect(historyGoBack).toHaveBeenCalled(); - expect(historyReplace).not.toHaveBeenCalledWith(PAGES.HOME_PAGE, { - tabValue: 2, - }); + expect(historyReplace).not.toHaveBeenCalledWith( + expect.objectContaining({ + pathname: PAGES.HOME_PAGE, + state: { tabValue: 2 }, + }), + ); }); test('shows assigned badge only for assigned lessons', async () => { @@ -318,7 +326,7 @@ describe('QRAssignments – full coverage', () => { await userEvent.click(screen.getByRole('button', { name: /Assign/i })); - const payload = historyPush.mock.calls[0][1]; + const payload = historyPush.mock.calls[0][0].state; const selectedIds = payload.selectedAssignments.manual['course-1'] .count as string[]; const manualLessons = payload.manualAssignments['course-1'] diff --git a/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.test.tsx b/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.test.tsx index 7f54a6ba78..5069f04bde 100644 --- a/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.test.tsx +++ b/src/teachers-module/components/homePage/assignment/TeacherRecommendedAssignments.test.tsx @@ -138,9 +138,13 @@ describe('TeacherRecommendedAssignments – full coverage', () => { await userEvent.click(screen.getByTestId('assign-btn')); expect(historyReplace).toHaveBeenCalledWith( - PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, expect.objectContaining({ - selectedAssignments: expect.any(Object), + pathname: PAGES.SHOW_STUDENTS_IN_ASSIGNED_PAGE, + search: '', + hash: '', + state: expect.objectContaining({ + selectedAssignments: expect.any(Object), + }), }), ); }); From 5ffb919592ebddd57a0c4c514e3dd72b98d50aa8 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Fri, 10 Jul 2026 18:49:06 +0530 Subject: [PATCH 5/7] fix(router): preserve legacy web deep links under hash routing --- src/App.test.tsx | 16 ++++- src/App.tsx | 2 + src/app/AppContent.tsx | 1 + .../components/ClassCodeGenerateButton.tsx | 11 +++- .../assignment/CreateSelectedAssignment.tsx | 11 +++- src/tests/__mocks__/@ionic/react-router.tsx | 2 +- src/utility/routerLocation.test.ts | 41 +++++++++++++ src/utility/routerLocation.ts | 58 ++++++++++++++++++- 8 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 src/utility/routerLocation.test.ts diff --git a/src/App.test.tsx b/src/App.test.tsx index f922ed67c3..c0ebf97dac 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -61,6 +61,16 @@ describe('App Component', () => { }); }); + // Covers: rewrites legacy pathname deep links into hash-router URLs before router mount. + it('rewrites legacy pathname deep links to hash routes on startup', () => { + window.history.replaceState({}, '', '/join-class?classCode=123'); + + renderWithProviders(); + + expect(window.location.pathname).toBe('/'); + expect(window.location.hash).toBe('#/join-class?classCode=123'); + }); + const popupConfigBase = { id: 'gb-popup-app-1', isActive: true, @@ -117,7 +127,7 @@ describe('App Component', () => { (key, defaultValue) => key === GENERIC_POP_UP ? popupConfig : defaultValue, ); - window.history.replaceState({}, '', `/?tab=${tab}`); + window.history.replaceState({}, '', `/#/?tab=${tab}`); renderWithProviders(); @@ -143,7 +153,7 @@ describe('App Component', () => { (key, defaultValue) => key === GENERIC_POP_UP ? popupConfig : defaultValue, ); - window.history.replaceState({}, '', '/?tab=home'); + window.history.replaceState({}, '', '/#/?tab=home'); renderWithProviders(); await new Promise((resolve) => setTimeout(resolve, 50)); @@ -164,7 +174,7 @@ describe('App Component', () => { key === GENERIC_POP_UP ? payload : defaultValue, ); - window.history.replaceState({}, '', '/?tab=leaderboard'); + window.history.replaceState({}, '', '/#/?tab=leaderboard'); renderWithProviders(); await new Promise((resolve) => setTimeout(resolve, 50)); diff --git a/src/App.tsx b/src/App.tsx index a7a37932e9..81bb2da4bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,12 +45,14 @@ import { useGrowthBookFeatureCache } from './hooks/useGrowthBookFeatureCache'; import { useHotUpdate } from './hooks/useHotUpdate'; import { useNativeAppListeners } from './hooks/useNativeAppListeners'; import { useRemoteAssetFlags } from './hooks/useRemoteAssetFlags'; +import { normalizeInitialHashRouteEntry } from './utility/routerLocation'; import AppContent from './app/AppContent'; setupIonicReact(); const App: React.FC = () => { + normalizeInitialHashRouteEntry(); useGrowthBookFeatureCache(); useRemoteAssetFlags(); useGlobalBrowserEffects(); diff --git a/src/app/AppContent.tsx b/src/app/AppContent.tsx index 92e1c55a16..a31e35143c 100644 --- a/src/app/AppContent.tsx +++ b/src/app/AppContent.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { IonRouterOutlet } from '@ionic/react'; import { HardwareBackButtonHandler } from '../common/backButtonRegistry'; import TermsGate from '../components/termsandconditons/TermsGate'; diff --git a/src/teachers-module/components/ClassCodeGenerateButton.tsx b/src/teachers-module/components/ClassCodeGenerateButton.tsx index a2cd01717b..9e9c2deecb 100644 --- a/src/teachers-module/components/ClassCodeGenerateButton.tsx +++ b/src/teachers-module/components/ClassCodeGenerateButton.tsx @@ -5,6 +5,8 @@ import { t } from 'i18next'; import { ServiceConfig } from '../../services/ServiceConfig'; import './ClassCodeGenerateButton.css'; // Ensure this still styles your component appropriately. import { Util } from '../../utility/util'; +import { buildHashAppUrl } from '../../utility/routerLocation'; +import { PAGES } from '../../common/constants'; interface ClassCodeProps { currentClassId: string; @@ -35,6 +37,13 @@ const ClassCodeGenerateButton: React.FC = ({ const shareClassCode = async () => { if (classCode) { const classCodeString = classCode.toString(); + const joinClassUrl = buildHashAppUrl( + { + pathname: PAGES.JOIN_CLASS, + search: `?classCode=${encodeURIComponent(classCodeString)}`, + }, + 'https://chimple.cc', + ).toString(); const message = `${t('Hi Students')}, ${t('To join the class')} "${className}", ${t('please install the Chimple Kids app:')} https://play.google.com/store/apps/details?id=org.chimple.bahama. @@ -46,7 +55,7 @@ const ClassCodeGenerateButton: React.FC = ({ Util.sendContentToAndroidOrWebShare( message, t('Hi Students'), - `https://chimple.cc/join-class?classCode=${classCodeString}`, + joinClassUrl, ); } }; diff --git a/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx b/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx index 5e6ed409d2..ae4edac037 100644 --- a/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx +++ b/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx @@ -31,6 +31,7 @@ import { triggerStreakRewardPulse, } from '../../../../common/streakRewardBridge'; import { parsePath } from 'history'; +import { buildHashAppUrl } from '../../../../utility/routerLocation'; interface LessonDetail { subject: string; @@ -479,9 +480,17 @@ const CreateSelectedAssignment = ({ text += `\n`; }); + const assignmentUrl = buildHashAppUrl( + { + pathname: PAGES.ASSIGNMENT, + search: `?batch_id=${encodeURIComponent(assignmentBatchId ?? '')}&source=teacher`, + }, + 'https://chimple.cc', + ).toString(); + text += `${translate( 'Please click this link to access your Homework', - )}: https://chimple.cc/assignment?batch_id=${assignmentBatchId}&source=teacher`; + )}: ${assignmentUrl}`; return text.trim(); }; diff --git a/src/tests/__mocks__/@ionic/react-router.tsx b/src/tests/__mocks__/@ionic/react-router.tsx index 1d45e7c608..4d41e9e0f4 100644 --- a/src/tests/__mocks__/@ionic/react-router.tsx +++ b/src/tests/__mocks__/@ionic/react-router.tsx @@ -10,7 +10,7 @@ const getInitialEntry = () => { return window.location.hash.slice(1); } - return `${window.location.pathname}${window.location.search}${window.location.hash}`; + return '/'; }; const MockIonicRouter: React.FC<{ children: React.ReactNode }> = ({ diff --git a/src/utility/routerLocation.test.ts b/src/utility/routerLocation.test.ts new file mode 100644 index 0000000000..198a0b7449 --- /dev/null +++ b/src/utility/routerLocation.test.ts @@ -0,0 +1,41 @@ +import { + buildHashAppUrl, + normalizeInitialHashRouteEntry, +} from './routerLocation'; + +describe('routerLocation', () => { + afterEach(() => { + window.history.replaceState({}, '', '/'); + }); + + test('buildHashAppUrl creates shareable hash-router URLs', () => { + const url = buildHashAppUrl( + { + pathname: '/assignment', + search: '?batch_id=batch-1&source=teacher', + }, + 'https://chimple.cc', + ); + + expect(url.toString()).toBe( + 'https://chimple.cc/#/assignment?batch_id=batch-1&source=teacher', + ); + }); + + test('normalizeInitialHashRouteEntry rewrites known pathname routes', () => { + window.history.replaceState({}, '', '/join-class?classCode=123'); + + expect(normalizeInitialHashRouteEntry()).toBe(true); + expect(window.location.pathname).toBe('/'); + expect(window.location.hash).toBe('#/join-class?classCode=123'); + }); + + test('normalizeInitialHashRouteEntry leaves root entry unchanged', () => { + window.history.replaceState({}, '', '/?page=/join-class'); + + expect(normalizeInitialHashRouteEntry()).toBe(false); + expect(window.location.pathname).toBe('/'); + expect(window.location.search).toBe('?page=/join-class'); + expect(window.location.hash).toBe(''); + }); +}); diff --git a/src/utility/routerLocation.ts b/src/utility/routerLocation.ts index 8b11ac6ce1..7539ea2c5e 100644 --- a/src/utility/routerLocation.ts +++ b/src/utility/routerLocation.ts @@ -1,6 +1,6 @@ import { createPath, parsePath } from 'history'; -import { BASE_NAME } from '../common/constants'; +import { BASE_NAME, PAGES } from '../common/constants'; type AppLocationParts = { pathname: string; @@ -60,6 +60,15 @@ const addBasename = (pathname: string) => { return pathname === '/' ? basename : `${basename}${pathname}`; }; +const pageRoutes = + PAGES && typeof PAGES === 'object' ? (PAGES as Record) : {}; + +const KNOWN_APP_ROUTE_PATHS = new Set( + Object.values(pageRoutes) + .map((path) => String(path)) + .filter((path) => path.startsWith('/') && path !== '/' && path !== '/#'), +); + const isHashRoutedUrl = () => typeof window !== 'undefined' && window.location.hash.startsWith('#/'); @@ -122,6 +131,26 @@ export const getAppSearchParams = () => new URLSearchParams(getAppSearch()); export const getAppPath = () => createPath(readCurrentAppLocation()); +export const buildHashAppUrl = ( + nextLocation: Partial = {}, + origin?: string, +) => { + const fallbackOrigin = + origin ?? + (typeof window !== 'undefined' + ? window.location.origin + : 'http://localhost'); + const url = new URL(addBasename('/'), fallbackOrigin); + + url.hash = createPath({ + pathname: normalizePathname(nextLocation.pathname ?? '/'), + search: normalizeSearch(nextLocation.search ?? ''), + hash: normalizeHash(nextLocation.hash ?? ''), + }); + + return url; +}; + export const buildAppUrl = (nextLocation: Partial = {}) => { if (typeof window === 'undefined') { return new URL('http://localhost/'); @@ -147,6 +176,33 @@ export const buildAppUrl = (nextLocation: Partial = {}) => { export const getAppHref = () => buildAppUrl().toString(); +export const normalizeInitialHashRouteEntry = () => { + if (typeof window === 'undefined' || isHashRoutedUrl()) { + return false; + } + + const legacyPathname = normalizePathname( + stripBasename(window.location.pathname || '/'), + ); + + if (!KNOWN_APP_ROUTE_PATHS.has(legacyPathname)) { + return false; + } + + const rewrittenUrl = buildHashAppUrl({ + pathname: legacyPathname, + search: window.location.search, + hash: window.location.hash, + }); + + window.history.replaceState( + window.history.state, + '', + rewrittenUrl.toString(), + ); + return true; +}; + export const replaceAppUrl = (nextLocation: Partial) => { const url = buildAppUrl(nextLocation); window.history.replaceState(window.history.state, '', url.toString()); From 61ba41fb29459a4fe3dc897b8bd8c073202de424 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Mon, 13 Jul 2026 10:28:33 +0530 Subject: [PATCH 6/7] configure router basename from constants --- src/App.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 81bb2da4bd..0420ce3e50 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ import { useRemoteAssetFlags } from './hooks/useRemoteAssetFlags'; import { normalizeInitialHashRouteEntry } from './utility/routerLocation'; import AppContent from './app/AppContent'; +import { BASE_NAME } from './common/constants'; setupIonicReact(); @@ -61,7 +62,7 @@ const App: React.FC = () => { return ( - + From 79fdbc9cb3fdc9f51cdd74ecfb9fcfd029874869 Mon Sep 17 00:00:00 2001 From: Manish Joshi Date: Mon, 20 Jul 2026 10:30:43 +0530 Subject: [PATCH 7/7] test: update test failing file profilemenu.test.tsx --- src/components/ProfileMenu/ProfileMenu.test.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/ProfileMenu/ProfileMenu.test.tsx b/src/components/ProfileMenu/ProfileMenu.test.tsx index 4aa8a9cfb7..247794edbc 100644 --- a/src/components/ProfileMenu/ProfileMenu.test.tsx +++ b/src/components/ProfileMenu/ProfileMenu.test.tsx @@ -226,9 +226,14 @@ describe('ProfileMenu Notification Logic', () => { expect(Util.setCurrentStudent).toHaveBeenCalledWith(null); }); expect(mockSetCurrentClass).not.toHaveBeenCalled(); - expect(mockReplace).toHaveBeenCalledWith(PAGES.SELECT_MODE, { - from: '/', - fromSchoolModeSwitchProfile: true, - }); + expect(mockReplace).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: PAGES.SELECT_MODE, + state: { + from: '/', + fromSchoolModeSwitchProfile: true, + }, + }), + ); }); });