diff --git a/src/components/layout/BottomNavbar.js b/src/components/layout/BottomNavbar.js index 85c96d7..c736d72 100644 --- a/src/components/layout/BottomNavbar.js +++ b/src/components/layout/BottomNavbar.js @@ -15,7 +15,14 @@ const BottomNavbar = ({navigation, routeName, routeParams}) => { const adminTabs = [ {label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')}, - {label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')}, + { + label: 'Map', + route: 'MapScreen', + onPress: () => navigation.navigate('MapScreen', { + selectedDepartmentSGid: null, + selectedDepartmentName: null, + }), + }, { label: 'Players', route: 'PlayersScreen', @@ -39,6 +46,11 @@ const BottomNavbar = ({navigation, routeName, routeParams}) => { ]; const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs; + const pendingTabs = [ + {label: 'Pending', route: 'TeamScreen', onPress: () => navigation.navigate('TeamScreen')}, + ...(inGame ? [{label: 'Leave', route: '__leave__', onPress: () => handleLeaveGame()}] : []), + ]; + const visibleTabs = (session.isPendingAdmin && !session.isAcceptedAdmin) ? pendingTabs : tabs; // State ---------------------- // Handlers ------------------- @@ -67,7 +79,7 @@ const BottomNavbar = ({navigation, routeName, routeParams}) => { return ( - {tabs.map((tab) => { + {visibleTabs.map((tab) => { const isActive = routeName === tab.route; const isLeave = tab.route === '__leave__'; return ( diff --git a/src/components/screens/LeaderboardScreen.js b/src/components/screens/LeaderboardScreen.js index e84061d..b7446f3 100644 --- a/src/components/screens/LeaderboardScreen.js +++ b/src/components/screens/LeaderboardScreen.js @@ -184,6 +184,14 @@ const LeaderboardScreen = ({navigation, route}) => { ); } + if (session.isPendingAdmin && !session.isAcceptedAdmin) { + return ( + + Waiting for admin approval. You will get access once approved. + + ); + } + if (isBusiness && isAdmin && !selectedDepartment) { return ( diff --git a/src/components/screens/MapScreen.js b/src/components/screens/MapScreen.js index ff8b843..4be100d 100644 --- a/src/components/screens/MapScreen.js +++ b/src/components/screens/MapScreen.js @@ -10,7 +10,7 @@ import ClaimTimerView from '../gameplay/ClaimTimerView'; import PlayerMapView from '../gameplay/PlayerMapView'; import useGameHook from '../../hooks/useGameHook'; import usePlayerGame from '../../hooks/usePlayerGame'; -import {getSession, setSelectedCache, setSessionGroup, setSessionTeam, setSessionUser} from '../../hooks/SessionStore'; +import {getSession, setPendingAdmin, setSelectedCache, setSessionGroup, setSessionTeam, setSessionUser} from '../../hooks/SessionStore'; import {getFovCone} from '../../utils/geoMath'; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05}; @@ -19,7 +19,7 @@ const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0. const MapScreen = ({navigation, route}) => { const session = getSession(); const isBusiness = Boolean(session.isBusiness); - const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups, getGroupByOrgCode} = useGameHook(); + const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, joinTeamByCode, createPrivateGame, getLobby, getUser, getSubgroups, getGroupByOrgCode} = useGameHook(); // Stable ref to prevent infinite re-render loops const getCachesRef = useRef(getCaches); @@ -104,8 +104,9 @@ const MapScreen = ({navigation, route}) => { return map; }, [departments]); const routeDepartmentSGid = route?.params?.selectedDepartmentSGid; + const isDepartmentScopedMap = routeDepartmentSGid !== null && routeDepartmentSGid !== undefined; const effectiveSGid = isBusiness - ? (isAdmin ? (selectedCacheSubgroupId ?? routeDepartmentSGid ?? defaultMemberSGid ?? null) : (session.currentSGid ?? null)) + ? (isAdmin ? (isDepartmentScopedMap ? routeDepartmentSGid : null) : (session.currentSGid ?? null)) : null; useEffect(() => { @@ -199,6 +200,7 @@ const MapScreen = ({navigation, route}) => { const result = await joinPrivateGame({JoinCode: gameCode.trim().toUpperCase(), Uid: session.currentUid}); if (!result) return; setSessionGroup(result.Gid, result.SGid); + setPendingAdmin(false); const freshUser = await getUser(session.currentUid); if (freshUser) { @@ -219,6 +221,7 @@ const MapScreen = ({navigation, route}) => { MaxMemberSubgroups: 1, }); if (!result) return; + setPendingAdmin(false); const freshUser = await getUser(session.currentUid); if (freshUser) { setSessionUser(freshUser); @@ -239,6 +242,7 @@ const MapScreen = ({navigation, route}) => { isBusiness: true, }); if (!result) return; + setPendingAdmin(false); const freshUser = await getUser(session.currentUid); if (freshUser) { setSessionUser(freshUser); @@ -265,8 +269,33 @@ const MapScreen = ({navigation, route}) => { const handleJoinDepartment = async () => { setJoinError(''); if (!deptCode.trim()) return; + const enteredCode = deptCode.trim().toUpperCase(); + const adminCode = String(matchedGroup?.AdminJoinCode || '').trim().toUpperCase(); + + // If the code matches the org admin code, submit an admin waitlist request + if (adminCode && enteredCode === adminCode) { + const waitlistResult = await joinTeamByCode({ + JoinCode: enteredCode, + Uid: session.currentUid, + ExpectedGid: matchedGroup.Gid, + }); + if (!waitlistResult || !waitlistResult.adminWaitlist) { + setJoinError('Could not submit admin request. Please try again.'); + return; + } + setSessionGroup(matchedGroup.Gid, null); + setSessionTeam(null); + setPendingAdmin(true); + setInGame(true); + setIsAdmin(false); + setJoinStep(null); + setDeptCode(''); + navigation.navigate('TeamScreen'); + return; + } + const result = await joinPrivateGame({ - JoinCode: deptCode.trim().toUpperCase(), + JoinCode: enteredCode, Uid: session.currentUid, ExpectedGid: matchedGroup.Gid, }); @@ -275,6 +304,7 @@ const MapScreen = ({navigation, route}) => { return; } setSessionGroup(result.Gid, result.SGid); + setPendingAdmin(false); const freshUser = await getUser(session.currentUid); if (freshUser) { setSessionUser(freshUser); @@ -684,6 +714,14 @@ const MapScreen = ({navigation, route}) => { ); } + if (session.isPendingAdmin && !session.isAcceptedAdmin) { + return ( + + Waiting for admin approval. You will get access once approved. + + ); + } + // Player — loading location if (playerLoading) { return ( diff --git a/src/components/screens/PlayersScreen.js b/src/components/screens/PlayersScreen.js index 9493193..969a44b 100644 --- a/src/components/screens/PlayersScreen.js +++ b/src/components/screens/PlayersScreen.js @@ -178,6 +178,14 @@ const PlayersScreen = ({navigation, route}) => { ); } + if (session.isPendingAdmin && !session.isAcceptedAdmin) { + return ( + + Waiting for admin approval. You will get access once approved. + + ); + } + if (isBusiness && isAdmin && !selectedDepartment) { return ( diff --git a/src/components/screens/TeamScreen.js b/src/components/screens/TeamScreen.js index f6ccfd1..cf29476 100644 --- a/src/components/screens/TeamScreen.js +++ b/src/components/screens/TeamScreen.js @@ -4,9 +4,9 @@ import Screen from '../layout/Screen'; import Card from '../UI/Card'; import {Button, ButtonTray} from '../UI/Button'; import useGameHook from '../../hooks/useGameHook'; -import {getSession, setSessionTeam} from '../../hooks/SessionStore'; +import {getSession, setPendingAdmin, setSessionTeam, setSessionUser} from '../../hooks/SessionStore'; -const TeamScreen = () => { +const TeamScreen = ({navigation}) => { const session = getSession(); const isAdmin = session.isAcceptedAdmin; const isBusiness = Boolean(session.isBusiness); @@ -48,6 +48,24 @@ const TeamScreen = () => { if (subgroup) teamsAreEnabled = Boolean(subgroup.TeamsEnabled); } + if (session.isPendingAdmin && !session.isAcceptedAdmin) { + const freshUser = await getUser(session.currentUid); + if (freshUser?.IsAcceptedAdmin) { + setPendingAdmin(false); + setSessionUser(freshUser); + setSessionTeam(freshUser.TGid ?? null); + navigation.reset({index: 0, routes: [{name: 'Admin'}]}); + return; + } + const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid); + if (waitlist.length > 0) { + setOnAdminWaitlist(true); + setLoading(false); + return; + } + setPendingAdmin(false); + } + if (!teamsAreEnabled) { setTeamsDisabled(true); setLoading(false); @@ -69,6 +87,44 @@ const TeamScreen = () => { load(); }, [activeTid]); + useEffect(() => { + if (!onAdminWaitlist && !session.isPendingAdmin) return; + + let stopped = false; + const refreshWaitlistStatus = async () => { + const currentSession = getSession(); + if (!currentSession.currentUid || !currentSession.currentGid) return; + + const freshUser = await getUser(currentSession.currentUid); + if (stopped) return; + + if (freshUser?.IsAcceptedAdmin) { + setPendingAdmin(false); + setSessionUser(freshUser); + setSessionTeam(freshUser.TGid ?? null); + navigation.reset({index: 0, routes: [{name: 'Admin'}]}); + return; + } + + const waitlist = await getAdminWaitlist(currentSession.currentGid, currentSession.currentUid); + if (stopped) return; + + const isStillWaiting = waitlist.length > 0; + setOnAdminWaitlist(isStillWaiting); + if (!isStillWaiting) { + setPendingAdmin(false); + } + }; + + refreshWaitlistStatus(); + const intervalId = setInterval(refreshWaitlistStatus, 5000); + + return () => { + stopped = true; + clearInterval(intervalId); + }; + }, [onAdminWaitlist, session.isPendingAdmin]); + const handleJoinTeam = async () => { if (!teamCode.trim()) return; // Force uppercase just in case a lowercase code is pasted @@ -82,6 +138,7 @@ const TeamScreen = () => { // If the server indicates this code matched the admin join code if (result.adminWaitlist) { + setPendingAdmin(true); setOnAdminWaitlist(true); return; } diff --git a/src/hooks/SessionStore.js b/src/hooks/SessionStore.js index 5e46ea9..a36b3a8 100644 --- a/src/hooks/SessionStore.js +++ b/src/hooks/SessionStore.js @@ -11,6 +11,7 @@ const session = { currentTid: null, isBusiness: null, isAcceptedAdmin: false, + isPendingAdmin: false, teamsEnabled: false, selectedCacheId: null, }; @@ -24,13 +25,16 @@ const setSessionUser = (user) => { session.currentTid = user?.TGid ?? null; session.isBusiness = user?.isBusiness ?? null; session.isAcceptedAdmin = Boolean(user?.IsAcceptedAdmin); + session.isPendingAdmin = Boolean( + user?.isBusiness && user?.Gid && !user?.SGid && !user?.IsAcceptedAdmin, + ); persistSession(); }; const setSessionGroup = (gid, sgid = null) => { session.currentGid = gid ?? null; - if (sgid !== null && sgid !== undefined) { - session.currentSGid = sgid; + if (sgid !== undefined) { + session.currentSGid = sgid ?? null; } persistSession(); }; @@ -45,6 +49,11 @@ const setSessionTeamsEnabled = (val) => { persistSession(); }; +const setPendingAdmin = (val) => { + session.isPendingAdmin = Boolean(val); + persistSession(); +}; + const setSelectedCache = (cacheId) => { session.selectedCacheId = cacheId ?? null; persistSession(); @@ -59,6 +68,7 @@ const clearSession = () => { session.currentTid = null; session.isBusiness = null; session.isAcceptedAdmin = false; + session.isPendingAdmin = false; AsyncStorage.removeItem(STORAGE_KEY).catch(() => {}); }; @@ -68,6 +78,7 @@ const clearGameSession = () => { session.currentSGid = null; session.currentTid = null; session.isAcceptedAdmin = false; + session.isPendingAdmin = false; session.teamsEnabled = false; session.selectedCacheId = null; persistSession(); @@ -92,4 +103,4 @@ const loadSession = async () => { return { ...session }; }; -export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, setSelectedCache, getSession, clearSession, clearGameSession, loadSession }; +export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, setPendingAdmin, setSelectedCache, getSession, clearSession, clearGameSession, loadSession }; diff --git a/src/hooks/useGameHook.js b/src/hooks/useGameHook.js index 795d1e5..4385a71 100644 --- a/src/hooks/useGameHook.js +++ b/src/hooks/useGameHook.js @@ -140,6 +140,9 @@ const useGameHook = () => { }; if (payload.JoinCode !== undefined) data.JoinCode = payload.JoinCode; if (payload.Tid !== undefined) data.Tid = payload.Tid; + if (payload.ExpectedGid !== undefined && payload.ExpectedGid !== null) { + data.ExpectedGid = payload.ExpectedGid; + } if (payload.ExpectedSGid !== undefined && payload.ExpectedSGid !== null) { data.ExpectedSGid = payload.ExpectedSGid; }