profile.tsx 4.9 KB

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