Pass profile to redeem Api and adapt to changed redeem flow

This commit is contained in:
Maximilian Harz 2025-06-27 16:47:05 +02:00
parent 5dba5e0ca9
commit 4209bf20d0
7 changed files with 76 additions and 25 deletions

View File

@ -46,7 +46,7 @@ import { itemsApi } from './api/itemsApi'
import { layersApi } from './api/layersApi'
import { mapApi } from './api/mapApi'
import { permissionsApi } from './api/permissionsApi'
import { userApi } from './api/userApi'
import { UserApi } from './api/userApi'
import { ModalContent } from './ModalContent'
import { Landingpage } from './pages/Landingpage'
import MapContainer from './pages/MapContainer'
@ -72,7 +72,8 @@ const UserSettings = lazy(() =>
})),
)
const inviteApi = new InviteApi()
const userApi = new UserApi()
const inviteApi = new InviteApi(userApi)
function App() {
const [permissionsApiInstance, setPermissionsApiInstance] = useState<permissionsApi>()
@ -160,7 +161,7 @@ function App() {
if (map && layers)
return (
<div className='App overflow-x-hidden'>
<AuthProvider userApi={new userApi()} inviteApi={inviteApi}>
<AuthProvider userApi={userApi} inviteApi={inviteApi}>
<AppShell
assetsApi={new assetsApi('https://api.utopia-lab.org/assets/')}
appName={map.name}

View File

@ -1,3 +1,5 @@
import type { UserApi } from 'utopia-ui'
import { config } from '@/config'
type InvitingProfileResponse = [
@ -7,6 +9,12 @@ type InvitingProfileResponse = [
]
export class InviteApi {
userApi: UserApi
constructor(userApi: UserApi) {
this.userApi = userApi
}
async validateInvite(inviteId: string): Promise<string | null> {
try {
const response = await fetch(
@ -36,24 +44,27 @@ export class InviteApi {
}
}
async redeemInvite(inviteId: string): Promise<string | null> {
async redeemInvite(inviteId: string, itemId: string): Promise<string | null> {
try {
const response = await fetch(
`${config.apiUrl}/flows/trigger/${config.redeemInviteFlowId}?secret=${inviteId}`,
{
method: 'GET',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
const token = await this.userApi.getToken()
if (!token) {
throw new Error('User is not authenticated. Cannot redeem invite.')
}
const response = await fetch(`${config.apiUrl}/flows/trigger/${config.redeemInviteFlowId}`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
)
body: JSON.stringify({ secret: inviteId, item: itemId }),
})
if (!response.ok) return null
const data = (await response.json()) as InvitingProfileResponse
return data[0].item
return (await response.json()) as string
} catch (error: unknown) {
// eslint-disable-next-line no-console
console.error('Error fetching inviting profile:', error)

View File

@ -8,7 +8,7 @@ import { createUser, passwordRequest, passwordReset, readMe, updateMe } from '@d
import { directusClient } from './directus'
import type { UserApi, UserItem } from 'utopia-ui'
import type { UserItem } from 'utopia-ui'
interface DirectusError {
errors: {
@ -17,7 +17,7 @@ interface DirectusError {
}[]
}
export class userApi implements UserApi {
export class UserApi {
async register(email: string, password: string, userName: string): Promise<any> {
try {
return await directusClient.request(createUser({ email, password, first_name: userName }))

View File

@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
import { MapOverlayPage } from '#components/Templates/MapOverlayPage'
import { useAuth } from './useAuth'
@ -21,22 +22,36 @@ export function LoginPage({ inviteApi }: Props) {
const { login, loading } = useAuth()
const myProfile = useMyProfile()
const navigate = useNavigate()
const redeemInvite = useCallback(
async (inviteCode: string): Promise<string | null> => {
if (!myProfile) {
toast.error('Could not find your profile to redeem the invite.')
return null
}
const invitingProfileId = await inviteApi.redeemInvite(inviteCode, myProfile.id)
localStorage.removeItem('inviteCode') // Clear invite code after redeeming
return invitingProfileId
},
[inviteApi, myProfile],
)
const handleSuccess = useCallback(async () => {
const inviteCode = localStorage.getItem('inviteCode')
let invitingProfileId: string | null = null
if (inviteCode) {
// If an invite code is stored, redeem it
invitingProfileId = await inviteApi.redeemInvite(inviteCode)
localStorage.removeItem('inviteCode') // Clear invite code after redeeming
invitingProfileId = await redeemInvite(inviteCode)
}
if (invitingProfileId) {
navigate(`/item/${invitingProfileId}`)
} else {
navigate('/')
}
}, [inviteApi, navigate])
}, [navigate, redeemInvite])
const onLogin = useCallback(async () => {
await toast.promise(login({ email, password }), {

View File

@ -0,0 +1,15 @@
import { useAuth } from '#components/Auth/useAuth'
import { useItems } from './useItems'
export const useMyProfile = () => {
const items = useItems()
const user = useAuth().user
// Find the user's profile item
const myProfile = items.find(
(item) => item.layer?.userProfileLayer && item.user_created?.id === user?.id,
)
return myProfile
}

View File

@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
import { MapOverlayPage } from '#components/Templates/MapOverlayPage'
import type { InviteApi } from '#types/InviteApi'
@ -19,13 +20,21 @@ export function InvitePage({ inviteApi }: Props) {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const myProfile = useMyProfile()
if (!id) throw new Error('Invite ID is required')
useEffect(() => {
async function redeemInvite() {
if (!id) throw new Error('Invite ID is required')
const invitingProfileId = await inviteApi.redeemInvite(id)
if (!myProfile) {
toast.error('Could not find your profile to redeem the invite.')
return
}
const invitingProfileId = await inviteApi.redeemInvite(id, myProfile.id)
if (invitingProfileId) {
toast.success('Invite redeemed successfully!')
navigate(`/item/${invitingProfileId}`)
@ -46,7 +55,7 @@ export function InvitePage({ inviteApi }: Props) {
// Redirect to login page
navigate('/login')
}
}, [id, isAuthenticated, inviteApi, navigate, isAuthenticationInitialized])
}, [id, isAuthenticated, inviteApi, navigate, isAuthenticationInitialized, myProfile])
return (
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>

View File

@ -1,4 +1,4 @@
export interface InviteApi {
validateInvite(inviteId: string): Promise<string | null>
redeemInvite(inviteId: string): Promise<string | null>
redeemInvite(inviteId: string, itemId: string): Promise<string | null>
}