mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-26 18:41:12 -04:00
Fix profile not found on some signup
This commit is contained in:
@@ -11,7 +11,7 @@ android {
|
||||
applicationId "com.compassconnections.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 53
|
||||
versionCode 54
|
||||
versionName "1.11.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"version": "1.22.0",
|
||||
"version": "1.22.1",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
|
||||
@@ -68,6 +68,7 @@ import {getNotifications} from './get-notifications'
|
||||
import {getProfileAnswers} from './get-profile-answers'
|
||||
import {getProfiles} from './get-profiles'
|
||||
import {getSupabaseToken} from './get-supabase-token'
|
||||
import {getUserAndProfileHandler} from './get-user-and-profile'
|
||||
import {getUserDataExport} from './get-user-data-export'
|
||||
import {hasFreeLike} from './has-free-like'
|
||||
import {health} from './health'
|
||||
@@ -398,6 +399,7 @@ const handlers: {[k in APIPath]: APIHandler<k>} = {
|
||||
'update-event': updateEvent,
|
||||
health: health,
|
||||
me: getMe,
|
||||
'get-user-and-profile': getUserAndProfileHandler,
|
||||
report: report,
|
||||
}
|
||||
|
||||
|
||||
68
backend/api/src/get-user-and-profile.ts
Normal file
68
backend/api/src/get-user-and-profile.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {debug} from 'common/logger'
|
||||
import {ProfileRow} from 'common/profiles/profile'
|
||||
import {convertUser} from 'common/supabase/users'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
import {type APIHandler} from './helpers/endpoint'
|
||||
|
||||
export async function getUserAndProfile(username: string) {
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
const user = await pg.oneOrNone('SELECT * FROM users WHERE username ILIKE $1', [username], (r) =>
|
||||
r ? convertUser(r) : null,
|
||||
)
|
||||
if (!user) return null
|
||||
|
||||
// Fetch profile like getProfileRow does
|
||||
const profileRes = await pg.oneOrNone<ProfileRow>('SELECT * FROM profiles WHERE user_id = $1', [
|
||||
user.id,
|
||||
])
|
||||
|
||||
if (!profileRes) return {user, profile: null}
|
||||
|
||||
// Parallel instead of sequential (like getProfileRow does in frontend)
|
||||
const [interestsRes, causesRes, workRes] = await Promise.all([
|
||||
pg.any(
|
||||
`SELECT interests.id
|
||||
FROM profile_interests
|
||||
JOIN interests ON profile_interests.option_id = interests.id
|
||||
WHERE profile_interests.profile_id = $1`,
|
||||
[profileRes.id],
|
||||
),
|
||||
pg.any(
|
||||
`SELECT causes.id
|
||||
FROM profile_causes
|
||||
JOIN causes ON profile_causes.option_id = causes.id
|
||||
WHERE profile_causes.profile_id = $1`,
|
||||
[profileRes.id],
|
||||
),
|
||||
pg.any(
|
||||
`SELECT work.id
|
||||
FROM profile_work
|
||||
JOIN work ON profile_work.option_id = work.id
|
||||
WHERE profile_work.profile_id = $1`,
|
||||
[profileRes.id],
|
||||
),
|
||||
])
|
||||
|
||||
const profileWithItems = {
|
||||
...profileRes,
|
||||
interests: interestsRes.map((r: any) => String(r.id)),
|
||||
causes: causesRes.map((r: any) => String(r.id)),
|
||||
work: workRes.map((r: any) => String(r.id)),
|
||||
}
|
||||
|
||||
return {user, profile: profileWithItems}
|
||||
}
|
||||
|
||||
export const getUserAndProfileHandler: APIHandler<'get-user-and-profile'> = async (
|
||||
{username},
|
||||
_auth,
|
||||
) => {
|
||||
const result = await getUserAndProfile(username)
|
||||
debug(result)
|
||||
return {
|
||||
user: result?.user,
|
||||
profile: result?.profile,
|
||||
}
|
||||
}
|
||||
@@ -1268,6 +1268,7 @@
|
||||
"profile.connect.tips": "- Wählen Sie den Verbindungstyp, für den Sie offen sind.\n- Sie sehen dies nicht, es sei denn, sie wählen denselben Typ wie Sie.\n- Wenn Sie beide denselben Typ wählen, werden Sie beide benachrichtigt.",
|
||||
"notifications.connection.mutual_title": "Es ist gegenseitig 🎉",
|
||||
"notifications.connection.mutual_body": "Du und {name} sind beide an einem {type} interessiert. Beginnen Sie das Gespräch.",
|
||||
"userpage.profileNotFound": "Profil nicht gefunden",
|
||||
"share_profile.on_x": "Auf X teilen",
|
||||
"share_profile.on_linkedin": "Auf LinkedIn teilen",
|
||||
"share_profile.view_profile_card": "Profilkarte ansehen",
|
||||
|
||||
@@ -1267,6 +1267,7 @@
|
||||
"profile.connect.tips": "- Vous choisissez le type de relation auquel vous êtes ouvert.\n- Ils ne verront pas ceci à moins qu'ils ne choisissent le même type que vous.\n- Si vous choisissez tous les deux le même type de relation, vous serez tous les deux notifiés.",
|
||||
"notifications.connection.mutual_title": "C’est mutuel 🎉",
|
||||
"notifications.connection.mutual_body": "{name} et vous êtes tous deux intéressés par un(e) {type}. Commencez la conversation.",
|
||||
"userpage.profileNotFound": "Profil introuvable",
|
||||
"share_profile.on_x": "Partager sur X",
|
||||
"share_profile.on_linkedin": "Partager sur LinkedIn",
|
||||
"share_profile.view_profile_card": "Voir la carte de profil",
|
||||
|
||||
@@ -239,6 +239,17 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Get the authenticated user full data',
|
||||
tag: 'Users',
|
||||
},
|
||||
'get-user-and-profile': {
|
||||
method: 'GET',
|
||||
authed: false,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
username: z.string().min(1),
|
||||
}),
|
||||
returns: {} as {user: User | null | undefined; profile: ProfileRow | null | undefined},
|
||||
summary: 'Get user and profile data by username',
|
||||
tag: 'Users',
|
||||
},
|
||||
'me/data': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
|
||||
@@ -6,10 +6,13 @@ export type ProfileRow = Row<'profiles'>
|
||||
export type ProfileWithoutUser = ProfileRow & {[K in OptionTableKey]?: string[]}
|
||||
export type Profile = ProfileWithoutUser & {user: User}
|
||||
|
||||
export const getProfileRow = async (
|
||||
export const getProfileRowWithFrontendSupabase = async (
|
||||
userId: string,
|
||||
db: SupabaseClient,
|
||||
): Promise<ProfileWithoutUser | null> => {
|
||||
// Do not use this method when running server-side (like in getStaticProps),
|
||||
// use the direct connection through the API via getProfileRow instead.
|
||||
|
||||
// Fetch profile
|
||||
const profileRes = await run(db.from('profiles').select('*').eq('user_id', userId))
|
||||
const profile = profileRes.data?.[0]
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {debug} from 'common/logger'
|
||||
import {getProfileRow, Profile, ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {
|
||||
getProfileRowWithFrontendSupabase,
|
||||
Profile,
|
||||
ProfileWithoutUser,
|
||||
} from 'common/profiles/profile'
|
||||
import {Row} from 'common/supabase/utils'
|
||||
import {User} from 'common/user'
|
||||
import {useEffect} from 'react'
|
||||
@@ -18,7 +22,7 @@ export const useProfile = () => {
|
||||
const refreshProfile = () => {
|
||||
if (user) {
|
||||
// logger.debug('Refreshing profile in useProfile for', user?.username, profile);
|
||||
getProfileRow(user.id, db).then((profile) => {
|
||||
getProfileRowWithFrontendSupabase(user.id, db).then((profile) => {
|
||||
if (!profile) setProfile(null)
|
||||
else setProfile(profile)
|
||||
})
|
||||
@@ -42,7 +46,7 @@ export const useProfileByUser = (user: User | undefined) => {
|
||||
function refreshProfile() {
|
||||
if (userId) {
|
||||
// console.debug('Refreshing profile in useProfileByUser for', user?.username, profile);
|
||||
getProfileRow(userId, db)
|
||||
getProfileRowWithFrontendSupabase(userId, db)
|
||||
.then((profile) => {
|
||||
if (!profile) setProfile(null)
|
||||
else setProfile({...profile, user})
|
||||
@@ -72,7 +76,7 @@ export const useProfileByUserId = (userId: string | undefined) => {
|
||||
useEffect(() => {
|
||||
// console.debug('Refreshing profile in useProfileByUserId for', userId, profile);
|
||||
if (userId)
|
||||
getProfileRow(userId, db).then((profile) => {
|
||||
getProfileRowWithFrontendSupabase(userId, db).then((profile) => {
|
||||
if (!profile) setProfile(null)
|
||||
else setProfile(profile)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {debug} from 'common/logger'
|
||||
import {getProfileRow} from 'common/profiles/profile'
|
||||
import {getProfileRowWithFrontendSupabase} from 'common/profiles/profile'
|
||||
import Router from 'next/router'
|
||||
import toast from 'react-hot-toast'
|
||||
import {firebaseLogin} from 'web/lib/firebase/users'
|
||||
@@ -25,7 +25,7 @@ export const googleSigninSignup = async () => {
|
||||
try {
|
||||
setOnboardingFlag()
|
||||
const creds = await firebaseLogin()
|
||||
await postSignupRedirect(creds)
|
||||
await postSignupRedirect(creds?.user?.uid)
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
toast.error('Failed to sign in: ' + e.message)
|
||||
@@ -36,11 +36,11 @@ export async function startSignup() {
|
||||
await Router.push('/register')
|
||||
}
|
||||
|
||||
export async function postSignupRedirect(creds: any) {
|
||||
const userId = creds?.user?.uid
|
||||
export async function postSignupRedirect(userId: string | undefined) {
|
||||
if (userId) {
|
||||
const profile = await getProfileRow(userId, db)
|
||||
const profile = await getProfileRowWithFrontendSupabase(userId, db)
|
||||
if (profile) {
|
||||
// Account already exists
|
||||
await Router.push('/')
|
||||
} else {
|
||||
await Router.push('/onboarding')
|
||||
|
||||
@@ -2,14 +2,14 @@ import {JSONContent} from '@tiptap/core'
|
||||
import {RESERVED_PATHS} from 'common/envs/constants'
|
||||
import {debug} from 'common/logger'
|
||||
import {getProfileOgImageUrl} from 'common/profiles/og-image'
|
||||
import {getProfileRow, ProfileRow} from 'common/profiles/profile'
|
||||
import {getUserForStaticProps} from 'common/supabase/users'
|
||||
import {ProfileRow} from 'common/profiles/profile'
|
||||
import {User} from 'common/user'
|
||||
import {unauthedApi} from 'common/util/api'
|
||||
import {parseJsonContentToText} from 'common/util/parse'
|
||||
import {GetStaticPropsContext} from 'next'
|
||||
import Head from 'next/head'
|
||||
import {useRouter} from 'next/router'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {BackButton} from 'web/components/back-button'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {PageBase} from 'web/components/page-base'
|
||||
@@ -21,44 +21,18 @@ import {useSaveReferral} from 'web/hooks/use-save-referral'
|
||||
import {useTracking} from 'web/hooks/use-tracking'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
import {safeLocalStorage} from 'web/lib/util/local'
|
||||
import {getPageData} from 'web/lib/util/page-data'
|
||||
import {isNativeMobile} from 'web/lib/util/webview'
|
||||
|
||||
import Custom404 from '../404'
|
||||
|
||||
async function getUser(username: string) {
|
||||
const user = await getUserForStaticProps(db, username)
|
||||
return user
|
||||
}
|
||||
|
||||
async function getProfile(userId: string) {
|
||||
const profile = await getProfileRow(userId, db)
|
||||
return profile
|
||||
async function getUserAndProfile(username: string) {
|
||||
return await unauthedApi('get-user-and-profile', {username})
|
||||
}
|
||||
|
||||
// getServerSideProps is a Next.js function that can be used to fetch data and render the contents of a page at request time.
|
||||
// export async function getServerSideProps(context: any) {
|
||||
// if (!isNativeMobile()) {
|
||||
// // Not mobile → let SSG handle it
|
||||
// return {notFound: true}
|
||||
// }
|
||||
//
|
||||
// // Mobile path: server-side fetch
|
||||
// const username = context.params.username
|
||||
// const user = await getUser(username)
|
||||
//
|
||||
// if (!user) {
|
||||
// return {props: {notFoundCustomText: 'User not found'}}
|
||||
// }
|
||||
//
|
||||
// const profile = await getProfile(user.id)
|
||||
//
|
||||
// console.log('getServerSideProps', {user, profile, username})
|
||||
//
|
||||
// return {props: {user, profile, username}}
|
||||
// }
|
||||
// export async function getServerSideProps(context: any) {}
|
||||
|
||||
// SSG: static site generation
|
||||
// Next.js will pre-render this page at build time using the props returned by getStaticProps
|
||||
@@ -77,12 +51,12 @@ export const getStaticProps = async (
|
||||
|
||||
console.log('Starting getStaticProps in /[username]', username)
|
||||
|
||||
const user = await getUser(username)
|
||||
const {user, profile} = await getUserAndProfile(username)
|
||||
|
||||
debug('getStaticProps', {user})
|
||||
debug('getStaticProps', {user, profile})
|
||||
|
||||
if (!user) {
|
||||
debug('No user', username)
|
||||
console.warn('No user found from getStaticProps:', username)
|
||||
return {
|
||||
props: {
|
||||
notFoundCustomText: null,
|
||||
@@ -112,8 +86,6 @@ export const getStaticProps = async (
|
||||
}
|
||||
}
|
||||
|
||||
const profile = await getProfile(user.id)
|
||||
|
||||
if (!profile) {
|
||||
debug('No profile', user.username)
|
||||
return {
|
||||
@@ -156,7 +128,6 @@ export default function UserPage(props: UserPageProps) {
|
||||
const router = useRouter()
|
||||
const t = useT()
|
||||
const username = (nativeMobile ? router.query.username : props.username) as string
|
||||
const [loading, setLoading] = useState(nativeMobile)
|
||||
const fromSignup = router?.query?.fromSignup === 'true'
|
||||
|
||||
// Hydrate from localStorage if coming from registration,
|
||||
@@ -176,31 +147,54 @@ export default function UserPage(props: UserPageProps) {
|
||||
return props
|
||||
})
|
||||
|
||||
const [loading, setLoading] = useState(!(fetchedProps.user && fetchedProps.profile))
|
||||
|
||||
useEffect(() => {
|
||||
if (fromSignup) return
|
||||
if (nativeMobile) {
|
||||
// Mobile/WebView scenario: fetch profile dynamically from the remote web server (to benefit from SSR and ISR)
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
|
||||
// Native mobile — fetch from remote web server (ISR)
|
||||
if (nativeMobile) {
|
||||
try {
|
||||
// console.log('Loading profile for native mobile', username)
|
||||
const _props = await getPageData(username)
|
||||
setFetchedProps(_props)
|
||||
if (_props?.user && _props?.profile) {
|
||||
setFetchedProps(_props)
|
||||
setLoading(false)
|
||||
return // ✅ done
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch profile for native mobile', e)
|
||||
setFetchedProps({
|
||||
username,
|
||||
notFoundCustomText: t('userpage.failedToFetch', 'Failed to fetch profile.'),
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
load()
|
||||
} else {
|
||||
// ISR returned null — fetch directly from backend
|
||||
if (props.notFoundCustomText === null) {
|
||||
try {
|
||||
const {user, profile} = await getUserAndProfile(username)
|
||||
if (user && profile) {
|
||||
setFetchedProps({username, user, profile})
|
||||
setLoading(false)
|
||||
return // ✅ done
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load profile from backend', e)
|
||||
}
|
||||
// All fallbacks exhausted
|
||||
setFetchedProps({
|
||||
...props,
|
||||
notFoundCustomText: t('userpage.profileNotFound', 'Profile not found.'),
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync new SSR props on navigation
|
||||
setFetchedProps(props)
|
||||
setLoading(false)
|
||||
}
|
||||
// On web, initialProfile from SSR/ISR is already loaded
|
||||
|
||||
load()
|
||||
}, [username, nativeMobile])
|
||||
|
||||
console.log(
|
||||
@@ -258,7 +252,7 @@ export default function UserPage(props: UserPageProps) {
|
||||
<PageBase trackPageView={'user page'} className={'relative p-2 sm:pt-0'}>
|
||||
<Col className="items-center justify-center h-full">
|
||||
<div className="text-xl font-semibold text-center mt-8">
|
||||
{t('userpage.profileNotCreated', "This user hasn't created a profile yet.")}
|
||||
{t('userpage.profileNotFound', 'Profile not found')}
|
||||
</div>
|
||||
</Col>
|
||||
</PageBase>
|
||||
@@ -269,7 +263,7 @@ export default function UserPage(props: UserPageProps) {
|
||||
}
|
||||
|
||||
function UserPageInner(props: ActiveUserPageProps) {
|
||||
// debug('Starting UserPageInner in /[username]')
|
||||
debug('Starting UserPageInner in /[username]', props)
|
||||
const {user, username} = props
|
||||
const router = useRouter()
|
||||
const t = useT()
|
||||
@@ -282,10 +276,17 @@ function UserPageInner(props: ActiveUserPageProps) {
|
||||
useSaveReferral(currentUser, {defaultReferrerUsername: username})
|
||||
useTracking('view profile', {username: user?.username})
|
||||
|
||||
const [staticProfile] = useState(props.profile && user ? {...props.profile, user: user} : null)
|
||||
// Recalculates on every props change
|
||||
const staticProfile = useMemo(
|
||||
() => (props.profile && user ? {...props.profile, user} : null),
|
||||
[props.profile, user],
|
||||
)
|
||||
|
||||
const {profile: clientProfile, refreshProfile} = useProfileByUser(user)
|
||||
|
||||
// Show the previous profile while loading another one
|
||||
const profile = clientProfile ?? staticProfile
|
||||
|
||||
// debug('profile:', user?.username, profile, clientProfile, staticProfile)
|
||||
|
||||
if (!isCurrentUser && profile?.disabled) {
|
||||
|
||||
@@ -38,7 +38,7 @@ function RegisterComponent() {
|
||||
// }
|
||||
|
||||
const checkProfileAndRedirect = async (creds: any) => {
|
||||
await postSignupRedirect(creds)
|
||||
await postSignupRedirect(creds?.user?.uid)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import {debug} from 'common/logger'
|
||||
import {getProfileRow} from 'common/profiles/profile'
|
||||
import {signInWithEmailAndPassword} from 'firebase/auth'
|
||||
import Link from 'next/link'
|
||||
import {useSearchParams} from 'next/navigation'
|
||||
import Router from 'next/router'
|
||||
import React, {Suspense, useEffect, useState} from 'react'
|
||||
import {GoogleButton} from 'web/components/buttons/sign-up-button'
|
||||
import FavIcon from 'web/components/FavIcon'
|
||||
@@ -15,7 +13,7 @@ import {useUser} from 'web/hooks/use-user'
|
||||
import {sendPasswordReset} from 'web/lib/firebase/password'
|
||||
import {auth, firebaseLogin} from 'web/lib/firebase/users'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
import {postSignupRedirect} from 'web/lib/util/signup'
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
@@ -46,14 +44,9 @@ function RegisterComponent() {
|
||||
if (userId) {
|
||||
debug('User signed in:', userId)
|
||||
try {
|
||||
const profile = await getProfileRow(userId, db)
|
||||
if (profile) {
|
||||
await Router.push('/')
|
||||
} else {
|
||||
await Router.push('/onboarding')
|
||||
}
|
||||
await postSignupRedirect(userId)
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile profile:', error)
|
||||
console.error('Error fetching profile:', error)
|
||||
}
|
||||
setIsLoading(false)
|
||||
setIsLoadingGoogle(false)
|
||||
|
||||
Reference in New Issue
Block a user