From aef0dba0b32d4a6244b4113edfa8ee902fdb73dd Mon Sep 17 00:00:00 2001 From: James Haworth Date: Sat, 14 Mar 2026 01:29:54 +0000 Subject: [PATCH 01/16] Refactor tab layout and enhance navigation experience - Updated tab layout to conditionally render NativeTabs or standard Tabs based on app ownership. - Replaced VersionAwareTabBar with direct tab configurations and integrated Ionicons for tab icons. - Added tab re-press listeners in Checkout and Settings screens to trigger refresh actions. - Removed unused components and optimized imports for better performance and clarity. - Enhanced bottom tab handling with safe area insets for improved layout consistency across devices. --- app.json | 2 +- app/(tabs)/_layout.tsx | 153 ++++++++++++++----------- app/(tabs)/checkout.tsx | 11 +- app/(tabs)/history/index.tsx | 9 +- app/(tabs)/settings.tsx | 20 +++- components/HapticTab.tsx | 18 --- components/ParallaxScrollView.tsx | 5 +- components/TabScreenScrollView.tsx | 11 +- components/VersionAwareTabBar.tsx | 23 ---- components/ui/TabBarBackground.ios.tsx | 46 -------- components/ui/TabBarBackground.tsx | 6 - 11 files changed, 136 insertions(+), 168 deletions(-) delete mode 100644 components/HapticTab.tsx delete mode 100644 components/VersionAwareTabBar.tsx delete mode 100644 components/ui/TabBarBackground.ios.tsx delete mode 100644 components/ui/TabBarBackground.tsx diff --git a/app.json b/app.json index 63542d3..f589c29 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "CheckOut", "slug": "CheckOut", - "version": "1.2.5", + "version": "1.2.6", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "checkoutattendance", diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index e3c6be3..8023935 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,24 +1,22 @@ import { Tabs, useNavigation } from 'expo-router'; +import { NativeTabs, Icon, Label, VectorIcon } from 'expo-router/unstable-native-tabs'; +import Ionicons from '@expo/vector-icons/Ionicons'; import { useRoute } from '@react-navigation/native'; import React, { useEffect, useRef } from 'react'; import { View, StyleSheet } from 'react-native'; +import Constants from 'expo-constants'; import { TopBar } from '@/components/TopBar'; import { ConnectionErrorBanner } from '@/components/ConnectionErrorBanner'; -import { VersionAwareTabBar } from '@/components/VersionAwareTabBar'; +import { VersionUpdateBanner } from '@/components/VersionUpdateBanner'; import { UnavailableOverlay } from '@/components/UnavailableOverlay'; import { ConnectionErrorOverlay } from '@/components/ConnectionErrorOverlay'; import { ForceUpdateOverlay } from '@/components/ForceUpdateOverlay'; import { useConnectionMonitor } from '@/hooks/useConnectionMonitor'; - -import { HapticTab } from '@/components/HapticTab'; -import { AutocheckinIcon } from '@/components/ui/AutocheckinIcon'; -import TabBarBackground from '@/components/ui/TabBarBackground'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { HistoryIcon } from '@/components/ui/HistoryIcon'; -import { SettingsIcon } from '@/components/ui/SettingsIcon'; -import { CheckOutIcon } from '@/components/ui/CheckOutIcon'; + +const isExpoGo = Constants.appOwnership === 'expo'; function TabLayoutContent() { const colorScheme = useColorScheme(); @@ -30,8 +28,6 @@ function TabLayoutContent() { // Handle tab refresh when connection is restored useEffect(() => { if (wasDisconnected && !previousWasDisconnectedRef.current) { - // Connection was just restored - refresh the current tab - // The index tab will check shouldAutoRefresh internally const currentParams = (route.params as any) || {}; navigation.setParams({ ...currentParams, @@ -41,63 +37,92 @@ function TabLayoutContent() { previousWasDisconnectedRef.current = wasDisconnected; }, [wasDisconnected, route, navigation]); + const tintColor = Colors[colorScheme ?? 'light'].tint; + return ( - } - screenOptions={{ - tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, - headerShown: false, - tabBarButton: HapticTab, - tabBarBackground: TabBarBackground, - }}> - - , - }} - listeners={({ navigation }) => ({ - tabPress: () => { - navigation.setParams({ refresh: Date.now() }); - }, - })} - /> - , - }} - /> - , - }} - /> - , - }} - listeners={({ navigation }) => ({ - tabPress: () => { - // Force a remount of the screen - navigation.setParams({ refresh: Date.now() }); - }, - })} - /> - + {isExpoGo ? ( + + + ( + + ), + }} + /> + ( + + ), + }} + /> + , + }} + /> + ( + + ), + }} + /> + + ) : ( + + + )} + + diff --git a/app/(tabs)/checkout.tsx b/app/(tabs)/checkout.tsx index 35fcf8e..23104dc 100644 --- a/app/(tabs)/checkout.tsx +++ b/app/(tabs)/checkout.tsx @@ -10,7 +10,7 @@ import { import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; import { TabScreenScrollView, TabScreenScrollViewRef } from '@/components/TabScreenScrollView'; -import { useFocusEffect, useRoute } from '@react-navigation/native'; +import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { useAppTheme } from '@/hooks/useAppTheme'; import { useLiveClasses } from '@/hooks/useLiveClasses'; import { useToast } from '@/hooks/useToast'; @@ -30,6 +30,7 @@ import type { Session, Code, AutoSplashResult } from '@/types/liveClasses'; export default function CheckoutScreen() { const { theme, isDark } = useAppTheme(); + const navigation = useNavigation(); const route = useRoute(); const toast = useToast(); const lastFocusRefreshRef = useRef(Date.now()); @@ -98,6 +99,14 @@ export default function CheckoutScreen() { }, [refresh, startPolling, stopPolling]) ); + // Listen for tab re-press to trigger refresh + useEffect(() => { + const unsubscribe = navigation.addListener('tabPress', () => { + navigation.setParams({ refresh: Date.now() }); + }); + return unsubscribe; + }, [navigation]); + // Listen for tab press when already focused const refreshParam = (route.params as { refresh?: number } | undefined)?.refresh; useEffect(() => { diff --git a/app/(tabs)/history/index.tsx b/app/(tabs)/history/index.tsx index 736bc81..2fc01e7 100644 --- a/app/(tabs)/history/index.tsx +++ b/app/(tabs)/history/index.tsx @@ -12,10 +12,11 @@ import { ThemedView } from '@/components/ThemedView'; import { Link } from 'expo-router'; import { createHistoryStyles } from '@/styles/history.styles'; import { useAppTheme } from '@/hooks/useAppTheme'; -import { useCallback, useState, useRef } from 'react'; +import { useCallback, useContext, useState, useRef } from 'react'; import { useHistory } from '@/hooks/useHistory'; +import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Colors } from '@/constants/Colors'; import { TabScreenScrollView } from '@/components/TabScreenScrollView'; import { useToast } from '@/hooks/useToast'; @@ -33,7 +34,9 @@ export default function HistoryScreen() { } = useHistory(); const { theme } = useAppTheme(); const styles = createHistoryStyles(theme as typeof Colors.light); - const tabBarHeight = useBottomTabBarHeight(); + const jsTabBarHeight = useContext(BottomTabBarHeightContext); + const { bottom: safeAreaBottom } = useSafeAreaInsets(); + const tabBarHeight = jsTabBarHeight ?? safeAreaBottom; const lastFocusRefreshRef = useRef(Date.now()); const toast = useToast(); const [loadingVisibility, setLoadingVisibility] = useState(null); // Track which code is being processed diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 863dd4d..fa5f119 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -9,7 +9,7 @@ import { Platform, } from 'react-native'; import { TabScreenScrollView } from '@/components/TabScreenScrollView'; -import { useFocusEffect } from '@react-navigation/native'; +import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; import { AuthModal } from '@/components/AuthModal'; @@ -27,6 +27,8 @@ import { Colors } from '@/constants/Colors'; import { router } from 'expo-router'; export default function SettingsScreen() { + const navigation = useNavigation(); + const route = useRoute(); const { signIn, logout, modalState, modalActions, isLoggedIn, getStoredToken } = useAuth(); const { accountInfo, fetchAccountInfo, clearAccountInfo, updateAccountInfo } = useSettingsAccount(); @@ -68,6 +70,22 @@ export default function SettingsScreen() { }, [getStoredToken]) ); + // Listen for tab re-press to trigger refresh + useEffect(() => { + const unsubscribe = navigation.addListener('tabPress', () => { + navigation.setParams({ refresh: Date.now() }); + }); + return unsubscribe; + }, [navigation]); + + // Handle refresh from tab re-press + const refreshParam = (route.params as { refresh?: number } | undefined)?.refresh; + useEffect(() => { + if (refreshParam) { + getStoredToken(); + } + }, [refreshParam, getStoredToken]); + // Fetch account info when login state changes useEffect(() => { if (isLoggedIn) { diff --git a/components/HapticTab.tsx b/components/HapticTab.tsx deleted file mode 100644 index 4be33a1..0000000 --- a/components/HapticTab.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; -import { PlatformPressable } from '@react-navigation/elements'; -import * as Haptics from 'expo-haptics'; - -export function HapticTab(props: BottomTabBarButtonProps) { - return ( - { - if (process.env.EXPO_OS === 'ios') { - // Add a soft haptic feedback when pressing down on the tabs. - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } - props.onPressIn?.(ev); - }} - /> - ); -} diff --git a/components/ParallaxScrollView.tsx b/components/ParallaxScrollView.tsx index 5df1d75..7a8e7a3 100644 --- a/components/ParallaxScrollView.tsx +++ b/components/ParallaxScrollView.tsx @@ -8,9 +8,12 @@ import Animated, { } from 'react-native-reanimated'; import { ThemedView } from '@/components/ThemedView'; -import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'; import { useColorScheme } from '@/hooks/useColorScheme'; +function useBottomTabOverflow() { + return 0; +} + const HEADER_HEIGHT = 250; type Props = PropsWithChildren<{ diff --git a/components/TabScreenScrollView.tsx b/components/TabScreenScrollView.tsx index bdbf288..18ac4f1 100644 --- a/components/TabScreenScrollView.tsx +++ b/components/TabScreenScrollView.tsx @@ -1,7 +1,8 @@ -import { useRef, useCallback, forwardRef, useImperativeHandle } from 'react'; +import { useRef, useCallback, useContext, forwardRef, useImperativeHandle } from 'react'; import { ScrollView, ScrollViewProps, Platform } from 'react-native'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useConnectionContext } from '@/contexts/ConnectionContext'; export type TabScreenScrollViewRef = { @@ -19,7 +20,9 @@ const BANNER_HEIGHT = 52; export const TabScreenScrollView = forwardRef( function TabScreenScrollView({ children, contentContainerStyle, ...props }, ref) { const scrollViewRef = useRef(null); - const tabBarHeight = useBottomTabBarHeight(); + const tabBarHeight = useContext(BottomTabBarHeightContext); + const { bottom: safeAreaBottom } = useSafeAreaInsets(); + const bottomInset = tabBarHeight ?? safeAreaBottom; const { connectionState } = useConnectionContext(); // Check if connection error banner should be shown @@ -27,7 +30,7 @@ export const TabScreenScrollView = forwardRef ({ diff --git a/components/VersionAwareTabBar.tsx b/components/VersionAwareTabBar.tsx deleted file mode 100644 index 4ee164d..0000000 --- a/components/VersionAwareTabBar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { BottomTabBarProps, BottomTabBar } from '@react-navigation/bottom-tabs'; - -import { VersionUpdateBanner } from './VersionUpdateBanner'; - -export function VersionAwareTabBar(props: BottomTabBarProps) { - return ( - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - backgroundColor: 'transparent', - }, - bannerWrapper: {}, -}); diff --git a/components/ui/TabBarBackground.ios.tsx b/components/ui/TabBarBackground.ios.tsx deleted file mode 100644 index f472243..0000000 --- a/components/ui/TabBarBackground.ios.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -import { StyleSheet } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useColorScheme } from '@/hooks/useColorScheme'; -import { BlurView } from 'expo-blur'; - -export default function TabBarBackground() { - const colorScheme = useColorScheme(); - - // Enhanced native iOS liquid glass effect - if (colorScheme === 'dark') { - return ( - - ); - } - - return ( - - ); -} - -export function useBottomTabOverflow() { - const tabHeight = useBottomTabBarHeight(); - const { bottom } = useSafeAreaInsets(); - return tabHeight - bottom; -} diff --git a/components/ui/TabBarBackground.tsx b/components/ui/TabBarBackground.tsx deleted file mode 100644 index 70d1c3c..0000000 --- a/components/ui/TabBarBackground.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// This is a shim for web and Android where the tab bar is generally opaque. -export default undefined; - -export function useBottomTabOverflow() { - return 0; -} From 375de7b1b2aa57ca982412dd95b913f94fe8f1de Mon Sep 17 00:00:00 2001 From: James Haworth Date: Sat, 14 Mar 2026 02:22:45 +0000 Subject: [PATCH 02/16] Enhance tab layout and improve screen responsiveness - Refactored TabLayoutContent to conditionally render TopBar and VersionUpdateBanner based on app ownership. - Updated CheckoutScreen and HistoryScreen to utilize safe area insets for better layout on various devices. - Adjusted loading indicators and error handling in CheckoutScreen for improved user experience. - Enhanced TabScreenScrollView to manage padding dynamically based on device type. - Added absolute positioning for TopBar and VersionUpdateBanner to ensure consistent visibility across screens. --- app/(tabs)/_layout.tsx | 172 ++++++++++++++++------------- app/(tabs)/checkout.tsx | 6 +- app/(tabs)/history/index.tsx | 14 +-- components/TabScreenScrollView.tsx | 17 ++- styles/history.styles.ts | 1 + 5 files changed, 121 insertions(+), 89 deletions(-) diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 8023935..f163c1f 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -41,88 +41,98 @@ function TabLayoutContent() { return ( - {isExpoGo ? ( - - - ( - - ), - }} - /> - ( - - ), - }} - /> - , - }} - /> - ( - - ), - }} - /> - + <> + + + + ( + + ), + }} + /> + ( + + ), + }} + /> + , + }} + /> + ( + + ), + }} + /> + + + ) : ( - - + + + + + + + )} - @@ -139,4 +149,18 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + absoluteTopBar: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 100, + }, + absoluteBottom: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + zIndex: 100, + }, }); diff --git a/app/(tabs)/checkout.tsx b/app/(tabs)/checkout.tsx index 23104dc..89e5df4 100644 --- a/app/(tabs)/checkout.tsx +++ b/app/(tabs)/checkout.tsx @@ -11,6 +11,7 @@ import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; import { TabScreenScrollView, TabScreenScrollViewRef } from '@/components/TabScreenScrollView'; import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useAppTheme } from '@/hooks/useAppTheme'; import { useLiveClasses } from '@/hooks/useLiveClasses'; import { useToast } from '@/hooks/useToast'; @@ -30,6 +31,7 @@ import type { Session, Code, AutoSplashResult } from '@/types/liveClasses'; export default function CheckoutScreen() { const { theme, isDark } = useAppTheme(); + const insets = useSafeAreaInsets(); const navigation = useNavigation(); const route = useRoute(); const toast = useToast(); @@ -425,7 +427,7 @@ export default function CheckoutScreen() { // Loading state if (isLoading && sessions.length === 0) { return ( - + ); @@ -434,7 +436,7 @@ export default function CheckoutScreen() { // Error state if (error && sessions.length === 0) { return ( - + {error} ); diff --git a/app/(tabs)/history/index.tsx b/app/(tabs)/history/index.tsx index 2fc01e7..da214b5 100644 --- a/app/(tabs)/history/index.tsx +++ b/app/(tabs)/history/index.tsx @@ -12,9 +12,8 @@ import { ThemedView } from '@/components/ThemedView'; import { Link } from 'expo-router'; import { createHistoryStyles } from '@/styles/history.styles'; import { useAppTheme } from '@/hooks/useAppTheme'; -import { useCallback, useContext, useState, useRef } from 'react'; +import { useCallback, useState, useRef } from 'react'; import { useHistory } from '@/hooks/useHistory'; -import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Colors } from '@/constants/Colors'; @@ -34,9 +33,7 @@ export default function HistoryScreen() { } = useHistory(); const { theme } = useAppTheme(); const styles = createHistoryStyles(theme as typeof Colors.light); - const jsTabBarHeight = useContext(BottomTabBarHeightContext); - const { bottom: safeAreaBottom } = useSafeAreaInsets(); - const tabBarHeight = jsTabBarHeight ?? safeAreaBottom; + const safeAreaInsets = useSafeAreaInsets(); const lastFocusRefreshRef = useRef(Date.now()); const toast = useToast(); const [loadingVisibility, setLoadingVisibility] = useState(null); // Track which code is being processed @@ -123,7 +120,8 @@ export default function HistoryScreen() { if (!historyData && isRefreshing) { return ( - + ); @@ -134,9 +132,7 @@ export default function HistoryScreen() { @@ -224,7 +220,7 @@ export default function HistoryScreen() { {/* Loading indicator at bottom */} {historyData.pagination.hasMore && ( - + {isLoadingMore ? ( diff --git a/components/TabScreenScrollView.tsx b/components/TabScreenScrollView.tsx index 18ac4f1..c72749e 100644 --- a/components/TabScreenScrollView.tsx +++ b/components/TabScreenScrollView.tsx @@ -3,8 +3,11 @@ import { ScrollView, ScrollViewProps, Platform } from 'react-native'; import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import Constants from 'expo-constants'; import { useConnectionContext } from '@/contexts/ConnectionContext'; +const isExpoGo = Constants.appOwnership === 'expo'; + export type TabScreenScrollViewRef = { scrollTo: (options: { y: number; animated?: boolean }) => void; scrollToEnd: (options?: { animated?: boolean }) => void; @@ -21,8 +24,11 @@ export const TabScreenScrollView = forwardRef(null); const tabBarHeight = useContext(BottomTabBarHeightContext); - const { bottom: safeAreaBottom } = useSafeAreaInsets(); - const bottomInset = tabBarHeight ?? safeAreaBottom; + const insets = useSafeAreaInsets(); + const bottomInset = tabBarHeight ?? insets.bottom; + // Only add top padding for native tabs where TopBar is absolute-positioned. + // In Expo Go, TopBar is a flex sibling so no extra padding is needed. + const topInset = isExpoGo ? 0 : insets.top + 60; const { connectionState } = useConnectionContext(); // Check if connection error banner should be shown @@ -62,8 +68,11 @@ export const TabScreenScrollView = forwardRef + scrollIndicatorInsets={{ top: topInset, bottom: totalBottomPadding }} + contentContainerStyle={[ + contentContainerStyle, + { paddingTop: topInset, paddingBottom: totalBottomPadding }, + ]}> {children} ); diff --git a/styles/history.styles.ts b/styles/history.styles.ts index 53f3c40..8c40be1 100644 --- a/styles/history.styles.ts +++ b/styles/history.styles.ts @@ -14,6 +14,7 @@ export const createHistoryStyles = (theme: ColorTheme) => alignItems: 'center', }, header: { + marginTop: 16, marginBottom: 16, }, title: { From e9f384fa1ac0d4ebd9af36b7fcda7c2c0528bbbf Mon Sep 17 00:00:00 2001 From: James Haworth Date: Sat, 14 Mar 2026 20:41:30 +0000 Subject: [PATCH 03/16] Enhance TabScreenScrollView with dynamic background color - Added useThemeColor hook to dynamically set the background color based on the current theme. - Updated ScrollView style to incorporate the dynamic background color, improving visual consistency across different themes. --- components/TabScreenScrollView.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/TabScreenScrollView.tsx b/components/TabScreenScrollView.tsx index c72749e..c3448f7 100644 --- a/components/TabScreenScrollView.tsx +++ b/components/TabScreenScrollView.tsx @@ -5,6 +5,7 @@ import { useFocusEffect } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Constants from 'expo-constants'; import { useConnectionContext } from '@/contexts/ConnectionContext'; +import { useThemeColor } from '@/hooks/useThemeColor'; const isExpoGo = Constants.appOwnership === 'expo'; @@ -30,6 +31,7 @@ export const TabScreenScrollView = forwardRef Date: Sat, 14 Mar 2026 20:45:51 +0000 Subject: [PATCH 04/16] Enhance tab navigation and layout responsiveness - Updated TabLayoutContent to include disableTransparentOnScrollEdge for improved tab visibility. - Refactored tab re-press listeners in CheckoutScreen and SettingsScreen to prevent unnecessary refresh actions when the tab is already focused. - Enhanced SettingsScreen to fetch account info upon refresh parameter change, improving data retrieval efficiency. - Modified ParallaxScrollView to utilize context for dynamic bottom tab overflow handling, ensuring better layout across devices. --- app/(tabs)/_layout.tsx | 2 +- app/(tabs)/checkout.tsx | 7 +++++-- app/(tabs)/settings.tsx | 10 +++++++--- components/ParallaxScrollView.tsx | 7 ++++++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index f163c1f..ab40e60 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -89,7 +89,7 @@ function TabLayoutContent() { ) : ( <> - + @@ -196,7 +205,7 @@ export function SessionCard({ {hasCode && onReportCode && ( - + onReportCode(primaryCode!)}> Report )} @@ -274,6 +283,13 @@ const styles = StyleSheet.create({ }, otherCodeCount: { fontSize: 12, + flex: 1, + textAlign: 'right', + }, + otherCodeReport: { + fontSize: 12, + fontWeight: '500', + marginLeft: 12, }, footer: { flexDirection: 'row', diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts index 24dcad8..af339a5 100644 --- a/hooks/useAuth.ts +++ b/hooks/useAuth.ts @@ -23,14 +23,20 @@ export function useAuth() { useEffect(() => { const initAuth = async () => { - const [storedToken, storedLoggedIn] = await Promise.all([ + const [storedToken, storedLoggedIn, storedAccountInfo] = await Promise.all([ storage.getItem(API_TOKEN_KEY), storage.getItem(IS_LOGGED_IN_KEY), + storage.getItem(ACCOUNT_INFO_KEY), ]); if (storedToken && storedLoggedIn === 'true') { setToken(storedToken); setIsLoggedIn(true); + if (storedAccountInfo) { + try { + setAccountInfo(JSON.parse(storedAccountInfo)); + } catch {} + } } }; From e6cea92a81cae9d52fbfa838d735754615ab8272 Mon Sep 17 00:00:00 2001 From: James Haworth Date: Sun, 15 Mar 2026 02:40:42 +0000 Subject: [PATCH 12/16] Remove unused constants from TabScreenScrollView and VersionUpdateBanner for improved code clarity and maintainability. --- components/TabScreenScrollView.tsx | 1 - components/VersionUpdateBanner.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/components/TabScreenScrollView.tsx b/components/TabScreenScrollView.tsx index d5f1db0..d18e399 100644 --- a/components/TabScreenScrollView.tsx +++ b/components/TabScreenScrollView.tsx @@ -23,7 +23,6 @@ type TabScreenScrollViewProps = ScrollViewProps & { // Height of the TopBar component (see components/TopBar.tsx) const TOP_BAR_HEIGHT = 60; - // ConnectionErrorBanner height: padding (12*2) + content height (~28) ≈ 52px const CONNECTION_BANNER_HEIGHT = 52; // VersionUpdateBanner height: padding (14*2) + content height (~24) ≈ 56px diff --git a/components/VersionUpdateBanner.tsx b/components/VersionUpdateBanner.tsx index 83fee09..1ecd1bd 100644 --- a/components/VersionUpdateBanner.tsx +++ b/components/VersionUpdateBanner.tsx @@ -8,8 +8,6 @@ import { getApiUrl } from '@/constants/api'; import { openAuthenticatedUrl } from '@/utils/api'; import { DEFAULT_TAB_BAR_HEIGHT } from '@/constants/Styles'; -// Approximate tab bar height (typical iOS tab bar is ~49px + safe area) -const DEFAULT_TAB_BAR_HEIGHT = 49; export function VersionUpdateBanner() { const { isDark } = useAppTheme(); From 4298746d9026d3851563cc4e716eecdcf256b6b0 Mon Sep 17 00:00:00 2001 From: James Haworth Date: Sun, 15 Mar 2026 02:42:46 +0000 Subject: [PATCH 13/16] Update app version to 1.2.7 and remove unused whitespace in VersionUpdateBanner for improved code clarity. --- app.json | 2 +- components/VersionUpdateBanner.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app.json b/app.json index f589c29..fca9a35 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "CheckOut", "slug": "CheckOut", - "version": "1.2.6", + "version": "1.2.7", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "checkoutattendance", diff --git a/components/VersionUpdateBanner.tsx b/components/VersionUpdateBanner.tsx index 1ecd1bd..64c3c60 100644 --- a/components/VersionUpdateBanner.tsx +++ b/components/VersionUpdateBanner.tsx @@ -8,7 +8,6 @@ import { getApiUrl } from '@/constants/api'; import { openAuthenticatedUrl } from '@/utils/api'; import { DEFAULT_TAB_BAR_HEIGHT } from '@/constants/Styles'; - export function VersionUpdateBanner() { const { isDark } = useAppTheme(); const { isOutdated, isForcedUpdate, latestVersion, changelog } = useVersionCheckContext(); From 91dfa770e72ebcd407ff014b1ed3fbe48cc9fbfd Mon Sep 17 00:00:00 2001 From: James Haworth Date: Mon, 16 Mar 2026 11:36:17 +0000 Subject: [PATCH 14/16] Refactor modal refresh handling in Checkout components - Updated CheckoutScreen to use specific modal identifiers for blocking and unblocking refresh actions, improving clarity and maintainability. - Enhanced OptionsSheet and ReportSheet to conditionally render reporting options based on the presence of a reporting function. - Introduced isAuthReady state in useAuth hook to ensure authentication readiness before checking login status, enhancing user experience during report submissions. --- app/(tabs)/checkout.tsx | 22 +++++++++++----------- components/checkout/OptionsSheet.tsx | 2 +- components/checkout/ReportSheet.tsx | 15 ++++++++++----- hooks/useAuth.ts | 8 +++++++- types/liveClasses.ts | 10 +++++++++- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/app/(tabs)/checkout.tsx b/app/(tabs)/checkout.tsx index 16f3394..5650a65 100644 --- a/app/(tabs)/checkout.tsx +++ b/app/(tabs)/checkout.tsx @@ -298,7 +298,7 @@ export default function CheckoutScreen() { setUndoModalSessionId(sessionId); setUndoModalTk(tk); setUndoModalVisible(true); - blockRefresh('modal'); + blockRefresh('undo-modal'); }, [blockRefresh] ); @@ -308,7 +308,7 @@ export default function CheckoutScreen() { setUndoModalVisible(false); setUndoModalSessionId(null); setUndoModalTk(null); - unblockRefresh('modal'); + unblockRefresh('undo-modal'); }, [unblockRefresh]); // Handle undo modal confirm - stays open until complete @@ -323,7 +323,7 @@ export default function CheckoutScreen() { setUndoModalVisible(false); setUndoModalSessionId(null); setUndoModalTk(null); - unblockRefresh('modal'); + unblockRefresh('undo-modal'); if (!result.success) { toast.error(result.error || 'Failed to undo submission'); @@ -344,7 +344,7 @@ export default function CheckoutScreen() { (session: Session) => { setSelectedSession(session); setOptionsSheetVisible(true); - blockRefresh('modal'); + blockRefresh('options-modal'); }, [blockRefresh] ); @@ -354,7 +354,7 @@ export default function CheckoutScreen() { (session: Session) => { setSelectedSession(session); setShareSheetVisible(true); - blockRefresh('modal'); + blockRefresh('share-modal'); }, [blockRefresh] ); @@ -364,7 +364,7 @@ export default function CheckoutScreen() { (code: Code) => { setSelectedCode(code); setVerificationSheetVisible(true); - blockRefresh('modal'); + blockRefresh('verification-modal'); }, [blockRefresh] ); @@ -373,19 +373,19 @@ export default function CheckoutScreen() { const closeOptions = useCallback(() => { setOptionsSheetVisible(false); setSelectedSession(null); - unblockRefresh('modal'); + unblockRefresh('options-modal'); }, [unblockRefresh]); const closeShare = useCallback(() => { setShareSheetVisible(false); setSelectedSession(null); - unblockRefresh('modal'); + unblockRefresh('share-modal'); }, [unblockRefresh]); const closeVerification = useCallback(() => { setVerificationSheetVisible(false); setSelectedCode(null); - unblockRefresh('modal'); + unblockRefresh('verification-modal'); }, [unblockRefresh]); // Open report sheet @@ -397,7 +397,7 @@ export default function CheckoutScreen() { setReportSession(session); setReportCode(targetCode); setReportSheetVisible(true); - blockRefresh('modal'); + blockRefresh('report-modal'); }, [blockRefresh] ); @@ -406,7 +406,7 @@ export default function CheckoutScreen() { setReportSheetVisible(false); setReportSession(null); setReportCode(null); - unblockRefresh('modal'); + unblockRefresh('report-modal'); }, [unblockRefresh]); // Handle input focus/blur diff --git a/components/checkout/OptionsSheet.tsx b/components/checkout/OptionsSheet.tsx index 6f73378..f711cb8 100644 --- a/components/checkout/OptionsSheet.tsx +++ b/components/checkout/OptionsSheet.tsx @@ -76,7 +76,7 @@ export function OptionsSheet({ )} {/* Report incorrect code */} - {primaryCode && ( + {primaryCode && onReportCode && ( { setError(null); - if (!isLoggedIn && !EMAIL_REGEX.test(email)) { + if (isAuthReady && !isLoggedIn && !EMAIL_REGEX.test(email)) { setError('Please enter a valid email address'); return; } + if (!selectedCode.codeIDs?.[0]) { + setError('No code ID available. Please try again.'); + return; + } + setIsSubmitting(true); try { const body: Record = { issueType: 'report-code', - codeId: selectedCode.codeIDs?.[0] || '', + codeId: selectedCode.codeIDs[0], chc: selectedCode.checkinCode, }; if (message.trim()) body.message = message.trim(); - if (!isLoggedIn && email) body.email = email; + if (isAuthReady && !isLoggedIn && email) body.email = email; const result = await checkoutApi('/api/support/submit', { method: 'POST', @@ -174,7 +179,7 @@ export function ReportSheet({ visible, onClose, session, codes, initialCode }: R /> {/* Email input (only when not logged in) */} - {!isLoggedIn && ( + {isAuthReady && !isLoggedIn && ( (null); const [username, setUsername] = useState(null); const [accountInfo, setAccountInfo] = useState(null); + const [isAuthReady, setIsAuthReady] = useState(false); useEffect(() => { const initAuth = async () => { @@ -35,9 +36,13 @@ export function useAuth() { if (storedAccountInfo) { try { setAccountInfo(JSON.parse(storedAccountInfo)); - } catch {} + } catch { + setAccountInfo(null); + await storage.deleteItem(ACCOUNT_INFO_KEY); + } } } + setIsAuthReady(true); }; initAuth(); @@ -210,6 +215,7 @@ export function useAuth() { logout, getStoredToken, isLoggedIn, + isAuthReady, isLoggingOut, accountInfo, modalState: { diff --git a/types/liveClasses.ts b/types/liveClasses.ts index dbe5c55..abdee26 100644 --- a/types/liveClasses.ts +++ b/types/liveClasses.ts @@ -127,7 +127,15 @@ export interface SubmissionState { } // Refresh blocker reasons -export type RefreshBlocker = 'typing' | 'submission' | 'modal' | 'interaction'; +export type RefreshBlocker = + | 'typing' + | 'submission' + | 'undo-modal' + | 'options-modal' + | 'share-modal' + | 'verification-modal' + | 'report-modal' + | 'interaction'; // Live classes state for the hook export interface LiveClassesState { From 4d880b77765a73a861b8d15c6529003c228c48f9 Mon Sep 17 00:00:00 2001 From: James Haworth Date: Mon, 16 Mar 2026 12:47:15 +0000 Subject: [PATCH 15/16] Enhance authentication readiness in ReportSheet and useAuth hook - Updated ReportSheet to disable the submit button until authentication is ready, improving user experience during report submissions. - Refactored useAuth hook to ensure isAuthReady is set after attempting to retrieve authentication data, enhancing the flow of authentication state management. --- components/checkout/ReportSheet.tsx | 2 +- hooks/useAuth.ts | 35 ++++++++++++++++------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/components/checkout/ReportSheet.tsx b/components/checkout/ReportSheet.tsx index 112ab14..b055381 100644 --- a/components/checkout/ReportSheet.tsx +++ b/components/checkout/ReportSheet.tsx @@ -215,7 +215,7 @@ export function ReportSheet({ visible, onClose, session, codes, initialCode }: R {isSubmitting ? ( diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts index 394064b..63b502a 100644 --- a/hooks/useAuth.ts +++ b/hooks/useAuth.ts @@ -24,25 +24,28 @@ export function useAuth() { useEffect(() => { const initAuth = async () => { - const [storedToken, storedLoggedIn, storedAccountInfo] = await Promise.all([ - storage.getItem(API_TOKEN_KEY), - storage.getItem(IS_LOGGED_IN_KEY), - storage.getItem(ACCOUNT_INFO_KEY), - ]); - - if (storedToken && storedLoggedIn === 'true') { - setToken(storedToken); - setIsLoggedIn(true); - if (storedAccountInfo) { - try { - setAccountInfo(JSON.parse(storedAccountInfo)); - } catch { - setAccountInfo(null); - await storage.deleteItem(ACCOUNT_INFO_KEY); + try { + const [storedToken, storedLoggedIn, storedAccountInfo] = await Promise.all([ + storage.getItem(API_TOKEN_KEY), + storage.getItem(IS_LOGGED_IN_KEY), + storage.getItem(ACCOUNT_INFO_KEY), + ]); + + if (storedToken && storedLoggedIn === 'true') { + setToken(storedToken); + setIsLoggedIn(true); + if (storedAccountInfo) { + try { + setAccountInfo(JSON.parse(storedAccountInfo)); + } catch { + setAccountInfo(null); + await storage.deleteItem(ACCOUNT_INFO_KEY); + } } } + } finally { + setIsAuthReady(true); } - setIsAuthReady(true); }; initAuth(); From a3bcacc66db7af9eb27ee7c928a57eddc15d11aa Mon Sep 17 00:00:00 2001 From: James Haworth Date: Mon, 16 Mar 2026 13:04:10 +0000 Subject: [PATCH 16/16] Refactor ReportSheet and enhance error handling in useAuth hook - Updated ReportSheet to reset the selected code when the sheet opens, improving user experience during code reporting. - Enhanced useAuth hook to log errors during authentication hydration, ensuring better debugging and state management. --- components/checkout/ReportSheet.tsx | 6 ++++-- hooks/useAuth.ts | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/components/checkout/ReportSheet.tsx b/components/checkout/ReportSheet.tsx index b055381..9e21c97 100644 --- a/components/checkout/ReportSheet.tsx +++ b/components/checkout/ReportSheet.tsx @@ -29,9 +29,11 @@ export function ReportSheet({ visible, onClose, session, codes, initialCode }: R const [isSubmitting, setIsSubmitting] = useState(false); const [selectedCode, setSelectedCode] = useState(initialCode); - // Reset state when sheet closes + // Reset state when sheet opens/closes useEffect(() => { - if (!visible) { + if (visible) { + setSelectedCode(initialCode); + } else { setMessage(''); setEmail(''); setError(null); diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts index 63b502a..e7c6ab7 100644 --- a/hooks/useAuth.ts +++ b/hooks/useAuth.ts @@ -43,6 +43,11 @@ export function useAuth() { } } } + } catch (error) { + console.error('Auth hydration failed:', error); + setToken(null); + setIsLoggedIn(false); + setAccountInfo(null); } finally { setIsAuthReady(true); }