diff --git a/apps/expo/src/app/(auth)/Loader.tsx b/apps/expo/src/app/(auth)/Loader.tsx new file mode 100644 index 00000000..98c16d4a --- /dev/null +++ b/apps/expo/src/app/(auth)/Loader.tsx @@ -0,0 +1,22 @@ +import { ActivityIndicator } from 'react-native'; + +interface Status { + loading: boolean; + // TODO(@zkirby) + // isError: boolean; + // isSuccess: boolean; +} + +const Loader = ({ + status, + className, + children, +}: React.PropsWithChildren<{ + status: Status; + className?: string; +}>) => { + if (status.loading) return ; + return children; +}; + +export default Loader; diff --git a/apps/expo/src/app/(auth)/UserLoader.tsx b/apps/expo/src/app/(auth)/UserLoader.tsx new file mode 100644 index 00000000..740a1ae0 --- /dev/null +++ b/apps/expo/src/app/(auth)/UserLoader.tsx @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; +import { api } from '../../utils/api'; +import Loader from './Loader'; + +/** + * This component is responsible for ensuring that the user *in our DB* + * is created. This is different from (but 1:1 with) the Clerk user. + * + * It's important that this mounts high in the component tree and + * prevents any children that might make API calls from rendering + * since almost every API call relies on the User being created + * in our DB. + * + * TODO(@zkirby): Think up a better solution here. We're essentially blocking + * the entire app from rendering for no reason (other than when the user is first created). + * Likely, we're not using Clerk correctly or under utilizing some functionality. + */ +const UserLoader = ({ children }: React.PropsWithChildren) => { + const ensureUserIsCreated = api.user.create.useMutation(); + + useEffect(() => { + ensureUserIsCreated.mutate(); + }, []); + + return ( + // NOTE(@zkirby): Keep spinning until we're sure the user is created, + // if we use 'isFetching' here, we'll render sub-tree once until the + // api calls kicks off. + + {children} + + ); +}; + +export default UserLoader; diff --git a/apps/expo/src/app/(tabs)/(alerts)/_components/AlertListItem.tsx b/apps/expo/src/app/(tabs)/(alerts)/_components/AlertListItem.tsx index 3ceca376..f3e01f7e 100644 --- a/apps/expo/src/app/(tabs)/(alerts)/_components/AlertListItem.tsx +++ b/apps/expo/src/app/(tabs)/(alerts)/_components/AlertListItem.tsx @@ -20,7 +20,7 @@ const AlertListItem = ({ onSelfAssign, }: { alert: Alert; - user: User; + user?: User; onAck: () => void; onClose: () => void; onReopen: () => void; @@ -28,7 +28,7 @@ const AlertListItem = ({ }) => { const { status, title, createdAt, summary } = alert; const initials = - (user.firstName?.slice(0, 1) ?? '') + (user.lastName?.slice(0, 1) ?? ''); + (user?.firstName?.slice(0, 1) ?? '') + (user?.lastName?.slice(0, 1) ?? ''); const StatusColors = StatusToColor[status as keyof typeof StatusToColor]; const { LeftSwipe, RightSwipe, action } = createSwipeAnimations(status, { diff --git a/apps/expo/src/app/(tabs)/(alerts)/index.tsx b/apps/expo/src/app/(tabs)/(alerts)/index.tsx index 7efe16f3..2519b607 100644 --- a/apps/expo/src/app/(tabs)/(alerts)/index.tsx +++ b/apps/expo/src/app/(tabs)/(alerts)/index.tsx @@ -11,7 +11,6 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Stack } from 'expo-router'; import { api } from '~/utils/api'; -import { useUser } from '../../hooks/useUser'; import AlertListItem from './_components/AlertListItem'; import type { Alert } from './alerts.types'; import { useSearch } from './hooks/useSearch'; @@ -33,7 +32,7 @@ const AlertListPage = () => { }, ], }); - const user = useUser(); + const user = api.user.me.useQuery(); const updateAlert = api.alert.update.useMutation(); const update = useCallback(async (id: string, alert: Partial) => { @@ -42,8 +41,8 @@ const AlertListPage = () => { }, []); const isLoading = useMemo( - () => updateAlert.isPending || alerts.isFetching, - [alerts.isFetching, updateAlert.isPending], + () => updateAlert.isPending || alerts.isFetching || user.isFetching, + [alerts.isFetching, updateAlert.isPending, user.isFetching], ); return ( @@ -68,7 +67,7 @@ const AlertListPage = () => { ALERTS - {isLoading || !user ? ( + {isLoading ? ( ) : ( { renderItem={({ item }) => ( update(item.id, { status: 'ACKED' })} onReopen={() => update(item.id, { status: 'OPEN' })} onClose={() => update(item.id, { status: 'CLOSED' })} onSelfAssign={() => - update(item.id, { assignedToId: user.id }) + update(item.id, { assignedToId: user.data?.user?.id }) } /> )} diff --git a/apps/expo/src/app/(tabs)/(profile)/index.tsx b/apps/expo/src/app/(tabs)/(profile)/index.tsx index 0dab4632..c20dc7a9 100644 --- a/apps/expo/src/app/(tabs)/(profile)/index.tsx +++ b/apps/expo/src/app/(tabs)/(profile)/index.tsx @@ -2,7 +2,8 @@ import { useAuth } from '@clerk/clerk-expo'; import { Stack } from 'expo-router'; import { Button, Text, View } from 'react-native'; -import { useUser } from '../../hooks/useUser'; +import { useMemo } from 'react'; +import { api } from '../../../utils/api'; const SignOut = () => { const { isLoaded, signOut } = useAuth(); @@ -23,7 +24,11 @@ const SignOut = () => { }; const ProfilePage = () => { - const user = useUser(); + const apiUser = api.user.me.useQuery(); + + const user = useMemo(() => { + return apiUser.data?.user; + }, [apiUser.data]); return ( diff --git a/apps/expo/src/app/(tabs)/_layout.tsx b/apps/expo/src/app/(tabs)/_layout.tsx index 4c9ee506..97320fc1 100644 --- a/apps/expo/src/app/(tabs)/_layout.tsx +++ b/apps/expo/src/app/(tabs)/_layout.tsx @@ -2,8 +2,8 @@ import FeatherIcons from '@expo/vector-icons/Feather'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Text, View } from 'react-native'; +import { api } from '../../utils/api'; import { useNotification } from '../hooks/useNotification'; -import { useUser } from '../hooks/useUser'; import AlertListPage from './(alerts)'; import ProfilePage from './(profile)'; @@ -11,7 +11,7 @@ const Tab = createBottomTabNavigator(); const TabsLayout = () => { useNotification(); - const user = useUser(); + const user = api.user.me.useQuery(); return ( { options={{ tabBarIcon: ({ size }) => { const initials = - (user?.firstName?.charAt(0) ?? '') + - (user?.lastName?.charAt(0) ?? ''); + (user.data?.user?.firstName?.charAt(0) ?? '') + + (user.data?.user?.lastName?.charAt(0) ?? ''); return ( { > - - - - + + + + + + diff --git a/apps/expo/src/app/hooks/useUser.ts b/apps/expo/src/app/hooks/useUser.ts deleted file mode 100644 index 70142b05..00000000 --- a/apps/expo/src/app/hooks/useUser.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useState } from 'react'; - -import type { RouterOutputs } from '~/utils/api'; -import { api } from '~/utils/api'; - -/** - * Hook for retrieving or creating the user - */ -export function useUser() { - const [user, setUser] = useState(); - - const createOrGetUser = api.user.me.useMutation({ - onSuccess: (result) => result?.user && setUser(result.user), - }); - - useEffect(() => { - createOrGetUser.mutate(); - }, []); - - return user; -} diff --git a/apps/nextjs/src/app/_components/Button.tsx b/apps/nextjs/src/app/_components/Button.tsx index debfe458..eb6ea2c1 100644 --- a/apps/nextjs/src/app/_components/Button.tsx +++ b/apps/nextjs/src/app/_components/Button.tsx @@ -14,13 +14,12 @@ const Button = ({ onClick, disabled, children, -}: { +}: React.PropsWithChildren<{ type?: ButtonType; className?: string; onClick?: () => void | Promise; disabled?: boolean; - children: React.ReactNode | React.ReactNode[]; -}) => ( +}>) => (