profile.tsx148 lines · main
| 1 | // @ts-nocheck |
| 2 | import * as Sentry from '@sentry/nextjs' |
| 3 | import { useIsLoggedIn, useUser } from 'common' |
| 4 | import { useRouter } from 'next/router' |
| 5 | import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react' |
| 6 | import { toast } from 'sonner' |
| 7 | |
| 8 | import { useSignOut } from './auth' |
| 9 | import { getGitHubProfileImgUrl } from './github' |
| 10 | import { usePermissionsQuery } from '@/data/permissions/permissions-query' |
| 11 | import { useProfileCreateMutation } from '@/data/profile/profile-create-mutation' |
| 12 | import { useProfileIdentitiesQuery } from '@/data/profile/profile-identities-query' |
| 13 | import { useProfileQuery } from '@/data/profile/profile-query' |
| 14 | import type { Profile } from '@/data/profile/types' |
| 15 | import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' |
| 16 | import type { ResponseError } from '@/types' |
| 17 | |
| 18 | export type ProfileContextType = { |
| 19 | profile: Profile | undefined |
| 20 | error: ResponseError | null |
| 21 | isLoading: boolean |
| 22 | isError: boolean |
| 23 | isSuccess: boolean |
| 24 | } |
| 25 | |
| 26 | export const ProfileContext = createContext<ProfileContextType>({ |
| 27 | profile: undefined, |
| 28 | error: null, |
| 29 | isLoading: true, |
| 30 | isError: false, |
| 31 | isSuccess: false, |
| 32 | }) |
| 33 | |
| 34 | export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { |
| 35 | const user = useUser() |
| 36 | const isLoggedIn = useIsLoggedIn() |
| 37 | const router = useRouter() |
| 38 | const signOut = useSignOut() |
| 39 | |
| 40 | const { mutate: sendEvent } = useSendEventMutation() |
| 41 | const { mutate: createProfile, isPending: isCreatingProfile } = useProfileCreateMutation({ |
| 42 | onSuccess: () => { |
| 43 | sendEvent({ action: 'sign_up', properties: { category: 'conversion' } }) |
| 44 | |
| 45 | if (user) { |
| 46 | // Send an event to GTM, will do nothing if GTM is not enabled |
| 47 | const thisWindow = window as any |
| 48 | thisWindow.dataLayer = thisWindow.dataLayer || [] |
| 49 | thisWindow.dataLayer.push({ |
| 50 | event: 'sign_up', |
| 51 | email: user.email, |
| 52 | }) |
| 53 | } |
| 54 | }, |
| 55 | onError: (error) => { |
| 56 | if (error.code === 409) { |
| 57 | // [Joshen] There's currently an assumption that createProfile is getting triggered |
| 58 | // multiple times unnecessarily, although the tracing the code i can't see why this might |
| 59 | // be happening unless GET profile is somehow returning `User's profile not found` incorrectly |
| 60 | // Adding a Sentry capture + toast in hopes to catch this while developing on local / staging |
| 61 | Sentry.captureMessage('Profile already exists: ' + error.message) |
| 62 | if (process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod') { |
| 63 | toast.error('[DEV] createProfile called despite profile already exists: ' + error.message) |
| 64 | } |
| 65 | } else { |
| 66 | Sentry.captureMessage('Failed to create users profile: ' + error.message) |
| 67 | toast.error('Failed to create your profile. Please refresh to try again.') |
| 68 | } |
| 69 | }, |
| 70 | }) |
| 71 | |
| 72 | // Track telemetry for the current user |
| 73 | const { |
| 74 | error, |
| 75 | data: profile, |
| 76 | isPending: isLoadingProfile, |
| 77 | isError, |
| 78 | isSuccess, |
| 79 | } = useProfileQuery({ |
| 80 | enabled: isLoggedIn, |
| 81 | }) |
| 82 | |
| 83 | useEffect(() => { |
| 84 | if (!isError) return |
| 85 | // if the user does not yet exist, create a profile for them |
| 86 | if (error?.message === "User's profile not found") { |
| 87 | createProfile() |
| 88 | } |
| 89 | |
| 90 | // [Alaister] If the user has a bad auth token, auth-js won't know about it |
| 91 | // and will think the user is authenticated. Since fetching the profile happens |
| 92 | // on every page load, we can check for a 401 here and sign the user out if |
| 93 | // they have a bad token. |
| 94 | if (error?.code === 401) { |
| 95 | signOut().then(() => router.push('/sign-in')) |
| 96 | } |
| 97 | }, [error, signOut, router, createProfile, isError]) |
| 98 | |
| 99 | const { isInitialLoading: isLoadingPermissions } = usePermissionsQuery({ enabled: isLoggedIn }) |
| 100 | |
| 101 | const value = useMemo(() => { |
| 102 | const isLoading = isLoadingProfile || isCreatingProfile || isLoadingPermissions |
| 103 | |
| 104 | return { |
| 105 | error, |
| 106 | profile, |
| 107 | isLoading, |
| 108 | isError, |
| 109 | isSuccess, |
| 110 | } |
| 111 | }, [ |
| 112 | isLoadingProfile, |
| 113 | isCreatingProfile, |
| 114 | isLoadingPermissions, |
| 115 | profile, |
| 116 | error, |
| 117 | isError, |
| 118 | isSuccess, |
| 119 | ]) |
| 120 | |
| 121 | return <ProfileContext.Provider value={value}>{children}</ProfileContext.Provider> |
| 122 | } |
| 123 | |
| 124 | export const useProfile = () => useContext(ProfileContext) |
| 125 | |
| 126 | export function useProfileNameAndPicture(): { |
| 127 | username?: string |
| 128 | primaryEmail?: string |
| 129 | avatarUrl?: string |
| 130 | isLoading: boolean |
| 131 | } { |
| 132 | const { profile, isLoading: isLoadingProfile } = useProfile() |
| 133 | const { data: identitiesData, isPending: isLoadingIdentities } = useProfileIdentitiesQuery() |
| 134 | |
| 135 | const isGitHubProfile = profile?.auth0_id?.startsWith('github') |
| 136 | |
| 137 | const gitHubUsername = isGitHubProfile |
| 138 | ? identitiesData?.identities.find((x) => x.provider === 'github')?.identity_data?.user_name |
| 139 | : undefined |
| 140 | const avatarUrl = isGitHubProfile ? getGitHubProfileImgUrl(gitHubUsername) : undefined |
| 141 | |
| 142 | return { |
| 143 | username: profile?.username, |
| 144 | primaryEmail: profile?.primary_email, |
| 145 | avatarUrl, |
| 146 | isLoading: isLoadingProfile || isLoadingIdentities, |
| 147 | } |
| 148 | } |