diff --git a/.codex b/.codex deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/App.test.tsx b/src/App.test.tsx index 5a8f9be5ca..c0ebf97dac 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; }); @@ -65,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,14 +123,13 @@ describe('App Component', () => { triggers: trigger, }; - mockGrowthbook.getFeatureValue.mockReturnValue(popupConfig); - window.history.replaceState({}, '', `/?tab=${tab}`); - - renderWithProviders( - - - , + (growthbookModule.useFeatureValue as jest.Mock).mockImplementation( + (key, defaultValue) => + key === GENERIC_POP_UP ? popupConfig : defaultValue, ); + window.history.replaceState({}, '', `/#/?tab=${tab}`); + + renderWithProviders(); await waitFor(() => expect(PopupManager.onAppOpen).toHaveBeenCalledWith(popupConfig), @@ -144,14 +149,13 @@ describe('App Component', () => { triggers: { type: 'APP_OPEN', value: 1 }, }; - mockGrowthbook.getFeatureValue.mockReturnValue(popupConfig); - window.history.replaceState({}, '', '/?tab=home'); - - renderWithProviders( - - - , + (growthbookModule.useFeatureValue as jest.Mock).mockImplementation( + (key, defaultValue) => + key === GENERIC_POP_UP ? popupConfig : defaultValue, ); + window.history.replaceState({}, '', '/#/?tab=home'); + + renderWithProviders(); await new Promise((resolve) => setTimeout(resolve, 50)); expect(PopupManager.onAppOpen).not.toHaveBeenCalled(); @@ -165,15 +169,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'); + 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 20053ccb82..0420ce3e50 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,8 +15,8 @@ * along with this program. If not, see . */ -import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; -import { IonReactRouter } from '@ionic/react-router'; +import { IonApp, setupIonicReact } from '@ionic/react'; +import { IonReactHashRouter } from '@ionic/react-router'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -39,37 +39,21 @@ 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'; -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'; +import { normalizeInitialHashRouteEntry } from './utility/routerLocation'; -setupIonicReact(); +import AppContent from './app/AppContent'; +import { BASE_NAME } from './common/constants'; -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(); - + normalizeInitialHashRouteEntry(); useGrowthBookFeatureCache(); useRemoteAssetFlags(); useGlobalBrowserEffects(); @@ -78,30 +62,9 @@ const App: React.FC = () => { return ( - - - - - - - - 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/app/AppContent.tsx b/src/app/AppContent.tsx new file mode 100644 index 0000000000..a31e35143c --- /dev/null +++ b/src/app/AppContent.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +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/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/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/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/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/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/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.test.tsx b/src/components/ProfileMenu/ProfileMenu.test.tsx index e1a6c72aac..247794edbc 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), + }), ); }); @@ -224,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, + }, + }), + ); }); }); 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/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.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('= 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/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/parent/ProfileCard.test.tsx b/src/components/parent/ProfileCard.test.tsx index 10512c33cf..6327978f86 100644 --- a/src/components/parent/ProfileCard.test.tsx +++ b/src/components/parent/ProfileCard.test.tsx @@ -86,9 +86,14 @@ describe('ProfileCard', () => { {}, ), ); - 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/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/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/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/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/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/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 29d45a2b5b..21916abb11 100644 --- a/src/ops-console/components/Sidebar.tsx +++ b/src/ops-console/components/Sidebar.tsx @@ -34,6 +34,8 @@ 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'; +import { parsePath } from 'history'; interface SidebarProps { name: string; @@ -115,7 +117,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); @@ -126,7 +131,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); @@ -175,7 +183,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/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/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..a088656eff 100644 --- a/src/pages/DisplayStudents.tsx +++ b/src/pages/DisplayStudents.tsx @@ -18,10 +18,13 @@ 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'; import './DisplayStudents.css'; +import { parsePath } from 'history'; + const DisplayStudents: FC<{}> = () => { const [isLoading, setIsLoading] = useState(true); const [students, setStudents] = useState[]>(); @@ -68,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; } @@ -104,11 +110,14 @@ 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 + window.location.search); + history.replace(PAGES.HOME + getAppSearch()); } }; return ( 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 2b166f2aff..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 { @@ -50,6 +49,8 @@ import { HomeworkPathwayLesson, } from '../components/assignment/homeworkPathwayHelpers'; import { replaceWithNavigationTarget } from '../helper/navigation/NavigationHandler'; +import { getAppSearchParams } from '../utility/routerLocation'; +import { PopupConfig } from '../components/GenericPopUp/GenericPopUpType'; const localData: any = {}; @@ -133,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 ( @@ -149,7 +146,7 @@ const Home: FC = () => { PopupManager.onAppOpen(popupConfig); PopupManager.onTimeElapsed(popupConfig); } - }, [growthbook, currentHeader]); + }, [currentHeader, popupConfig]); useEffect(() => { const student = Util.getCurrentStudent(); @@ -297,7 +294,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..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'; @@ -47,6 +48,9 @@ import { getBundleZipUrlsForEnv, REMOTE_CONFIG_KEYS, } 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 = @@ -135,7 +139,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); @@ -153,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 @@ -510,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); } @@ -521,7 +523,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), @@ -533,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; @@ -1328,7 +1330,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 +1366,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.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 eda01fff20..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,12 +21,15 @@ 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; 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); @@ -60,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) { @@ -123,31 +127,38 @@ const LiveQuizGame: FC = () => { setShowScoreCard(true); setShowDialogBox(true); - const popupConfig = growthbook?.getFeatureValue('generic-pop-up', null); - if (popupConfig) { PopupManager.onGameComplete(popupConfig); } }; const push = () => { - const urlParams = new URLSearchParams(window.location.search); + const urlParams = getAppSearchParams(); const fromPath: string = state?.from ?? PAGES.HOME; const returnState = { ...(state?.returnState ?? state), 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/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..893958fd80 100644 --- a/src/pages/LiveQuizRoom.tsx +++ b/src/pages/LiveQuizRoom.tsx @@ -12,6 +12,9 @@ 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'; +import { parsePath } from 'history'; + const LiveQuizRoom: React.FC = () => { const [students, setStudents] = useState( new Map>(), @@ -26,7 +29,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); @@ -158,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/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.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/pages/StickerBook.tsx b/src/pages/StickerBook.tsx index c11b78542f..c7e486fb2e 100644 --- a/src/pages/StickerBook.tsx +++ b/src/pages/StickerBook.tsx @@ -19,11 +19,13 @@ 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, } from '../utility/stickerBookAssets'; import { useStickerBookSave } from '../hooks/useStickerBookSave'; +import { parsePath } from 'history'; type CurrentProgress = { bookId: string; @@ -203,7 +205,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], ); @@ -250,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, + }, }); }; @@ -286,7 +291,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/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/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/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/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/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/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/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..ae4edac037 100644 --- a/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx +++ b/src/teachers-module/components/homePage/assignment/CreateSelectedAssignment.tsx @@ -30,6 +30,8 @@ import { getStreakTargetRect, triggerStreakRewardPulse, } from '../../../../common/streakRewardBridge'; +import { parsePath } from 'history'; +import { buildHashAppUrl } from '../../../../utility/routerLocation'; interface LessonDetail { subject: string; @@ -478,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(); }; @@ -916,11 +926,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 +946,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.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/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.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), + }), }), ); }); 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 109516e6cb..d055e9fec1 100644 --- a/src/teachers-module/pages/ReqEditSchool.tsx +++ b/src/teachers-module/pages/ReqEditSchool.tsx @@ -21,6 +21,8 @@ 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'; +import { parsePath } from 'history'; interface LocationState { school?: SchoolWithRole['school']; role?: RoleType; @@ -88,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); @@ -129,7 +133,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/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/tests/__mocks__/@ionic/react-router.tsx b/src/tests/__mocks__/@ionic/react-router.tsx index 7e47f9c755..4d41e9e0f4 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 '/'; }; + +const MockIonicRouter: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( + {children} +); + +export const IonReactHashRouter = MockIonicRouter; 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.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 new file mode 100644 index 0000000000..7539ea2c5e --- /dev/null +++ b/src/utility/routerLocation.ts @@ -0,0 +1,210 @@ +import { createPath, parsePath } from 'history'; + +import { BASE_NAME, PAGES } 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 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('#/'); + +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 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/'); + } + + 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 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()); + 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..63d79c1ae5 100644 --- a/src/utility/util.ts +++ b/src/utility/util.ts @@ -124,6 +124,12 @@ 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'; +import { parsePath } from 'history'; type LessonBundleDownloadOptions = { lessonId: string; @@ -1097,8 +1103,8 @@ export class Util { }); await FirebaseAnalytics.setScreenName({ - screenName: window.location.pathname, - nameOverride: window.location.pathname, + screenName: getAppPathname(), + nameOverride: getAppPathname(), }); await FirebaseAnalytics.logEvent({ @@ -1147,8 +1153,8 @@ export class Util { //Setting Screen Name await FirebaseAnalytics.setScreenName({ - screenName: window.location.pathname, - nameOverride: window.location.pathname, + screenName: getAppPathname(), + nameOverride: getAppPathname(), }); } @@ -1159,8 +1165,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 +1179,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 +1795,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(); @@ -2234,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( @@ -2251,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; }