From 5f32e5d0255e5e1e5295a134a2d1eb40ac1c5431 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 5 Mar 2026 17:51:25 +0100 Subject: [PATCH] Fix profile not found on some signup --- android/app/build.gradle | 2 +- backend/api/package.json | 2 +- backend/api/src/app.ts | 2 + backend/api/src/get-user-and-profile.ts | 68 +++++++++++++++ common/messages/de.json | 1 + common/messages/fr.json | 1 + common/src/api/schema.ts | 11 +++ common/src/profiles/profile.ts | 5 +- web/hooks/use-profile.ts | 12 ++- web/lib/util/signup.ts | 10 +-- web/pages/[username]/index.tsx | 111 ++++++++++++------------ web/pages/register.tsx | 2 +- web/pages/signin.tsx | 13 +-- 13 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 backend/api/src/get-user-and-profile.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index a0c286dd..6af00daf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 { diff --git a/backend/api/package.json b/backend/api/package.json index ff200145..01938a87 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -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", diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index ea9a3936..1da2d067 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.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} = { 'update-event': updateEvent, health: health, me: getMe, + 'get-user-and-profile': getUserAndProfileHandler, report: report, } diff --git a/backend/api/src/get-user-and-profile.ts b/backend/api/src/get-user-and-profile.ts new file mode 100644 index 00000000..45e5007d --- /dev/null +++ b/backend/api/src/get-user-and-profile.ts @@ -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('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, + } +} diff --git a/common/messages/de.json b/common/messages/de.json index c8819a03..cae78495 100644 --- a/common/messages/de.json +++ b/common/messages/de.json @@ -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", diff --git a/common/messages/fr.json b/common/messages/fr.json index 92978728..b8204056 100644 --- a/common/messages/fr.json +++ b/common/messages/fr.json @@ -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", diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 24ba40db..c2402952 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -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, diff --git a/common/src/profiles/profile.ts b/common/src/profiles/profile.ts index faa5cf06..f11afddf 100644 --- a/common/src/profiles/profile.ts +++ b/common/src/profiles/profile.ts @@ -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 => { + // 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] diff --git a/web/hooks/use-profile.ts b/web/hooks/use-profile.ts index a93c418e..faf35cd6 100644 --- a/web/hooks/use-profile.ts +++ b/web/hooks/use-profile.ts @@ -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) }) diff --git a/web/lib/util/signup.ts b/web/lib/util/signup.ts index 0519785c..6c5fe670 100644 --- a/web/lib/util/signup.ts +++ b/web/lib/util/signup.ts @@ -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') diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index 031b3a1c..d785d7a3 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -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) {
- {t('userpage.profileNotCreated', "This user hasn't created a profile yet.")} + {t('userpage.profileNotFound', 'Profile not found')}
@@ -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) { diff --git a/web/pages/register.tsx b/web/pages/register.tsx index f62419b9..5889c6ee 100644 --- a/web/pages/register.tsx +++ b/web/pages/register.tsx @@ -38,7 +38,7 @@ function RegisterComponent() { // } const checkProfileAndRedirect = async (creds: any) => { - await postSignupRedirect(creds) + await postSignupRedirect(creds?.user?.uid) setIsLoading(false) } diff --git a/web/pages/signin.tsx b/web/pages/signin.tsx index 3aadd1dc..e43db83b 100644 --- a/web/pages/signin.tsx +++ b/web/pages/signin.tsx @@ -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)