Fix profile not found on some signup

This commit is contained in:
MartinBraquet
2026-03-05 17:51:25 +01:00
parent b3d203afa2
commit 5f32e5d025
13 changed files with 162 additions and 78 deletions

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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,
}

View 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,
}
}

View File

@@ -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",

View File

@@ -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": "Cest 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",

View File

@@ -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,

View File

@@ -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]

View File

@@ -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)
})

View File

@@ -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')

View File

@@ -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) {

View File

@@ -38,7 +38,7 @@ function RegisterComponent() {
// }
const checkProfileAndRedirect = async (creds: any) => {
await postSignupRedirect(creds)
await postSignupRedirect(creds?.user?.uid)
setIsLoading(false)
}

View File

@@ -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)