mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-01-20 11:51:26 +00:00
222 lines
6.2 KiB
TypeScript
222 lines
6.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
import { toast } from 'react-toastify'
|
|
|
|
import { useAuth } from '#components/Auth/useAuth'
|
|
import { useUpdateItem } from '#components/Map/hooks/useItems'
|
|
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
|
|
import { MapOverlayPage } from '#components/Templates/MapOverlayPage'
|
|
|
|
import type { FullItemsApi } from '#types/FullItemsApi'
|
|
import type { InviteApi } from '#types/InviteApi'
|
|
import type { Item } from '#types/Item'
|
|
|
|
interface Props {
|
|
inviteApi: InviteApi
|
|
itemsApi: FullItemsApi<Item>
|
|
}
|
|
|
|
/**
|
|
* @category Onboarding
|
|
*/
|
|
export function InvitePage({ inviteApi, itemsApi }: Props) {
|
|
const { isAuthenticated, isInitialized: isAuthenticationInitialized } = useAuth()
|
|
const { id } = useParams<{ id: string }>()
|
|
const navigate = useNavigate()
|
|
const updateItem = useUpdateItem()
|
|
|
|
const { myProfile, isUserProfileLayerLoaded, createEmptyProfile } = useMyProfile()
|
|
|
|
if (!id) throw new Error('Invite ID is required')
|
|
|
|
const [invitingProfile, setInvitingProfile] = useState<Item | null>(null)
|
|
const [isRedeemingDone, setRedeemingDone] = useState(false)
|
|
const [isValidationDone, setValidationDone] = useState(false)
|
|
|
|
async function redeemInvite(id: string, myProfileId: string) {
|
|
const invitingProfileId = await inviteApi.redeemInvite(id, myProfileId)
|
|
|
|
if (invitingProfileId) {
|
|
toast.success('Invite redeemed successfully!')
|
|
localStorage.removeItem('inviteCode')
|
|
setRedeemingDone(true)
|
|
navigate(`/item/${invitingProfileId}`)
|
|
} else {
|
|
toast.error('Failed to redeem invite')
|
|
navigate('/')
|
|
}
|
|
}
|
|
|
|
const confirmFollowAsync = async () => {
|
|
if (!isAuthenticated) return
|
|
|
|
if (!isUserProfileLayerLoaded || isRedeemingDone) return
|
|
|
|
const myActualProfile = myProfile ?? (await createEmptyProfile())
|
|
|
|
if (!myActualProfile) {
|
|
toast.error('Failed to create profile')
|
|
return
|
|
}
|
|
|
|
if (!invitingProfile) {
|
|
toast.error('Inviting profile not found')
|
|
return
|
|
}
|
|
|
|
await redeemInvite(id, myActualProfile.id)
|
|
|
|
// Add new relation to local state
|
|
updateItem({
|
|
...myActualProfile,
|
|
relations: [
|
|
...(myActualProfile.relations ?? []),
|
|
{
|
|
type: 'is_following',
|
|
direction: 'outgoing',
|
|
related_items_id: invitingProfile.id,
|
|
},
|
|
],
|
|
})
|
|
|
|
setRedeemingDone(true)
|
|
}
|
|
|
|
const confirmFollow = () => {
|
|
void confirmFollowAsync()
|
|
}
|
|
|
|
useEffect(() => {
|
|
async function validateInvite(id: string) {
|
|
const invitingProfileId = await inviteApi.validateInvite(id)
|
|
|
|
if (!invitingProfileId) {
|
|
toast.error('Invalid invite code')
|
|
localStorage.removeItem('inviteCode')
|
|
navigate('/')
|
|
return
|
|
}
|
|
|
|
const invitingProfile = await itemsApi.getItem(invitingProfileId)
|
|
|
|
if (invitingProfileId === myProfile?.id) {
|
|
toast.error('You cannot invite yourself')
|
|
localStorage.removeItem('inviteCode')
|
|
// Navigate to own profile
|
|
navigate('/item/' + myProfile.id)
|
|
return
|
|
}
|
|
|
|
if (
|
|
myProfile?.relations?.some(
|
|
(r) => r.type === 'is_following' && r.related_items_id === invitingProfileId,
|
|
)
|
|
) {
|
|
toast.error('You are already following this profile')
|
|
localStorage.removeItem('inviteCode')
|
|
navigate('/item/' + invitingProfileId)
|
|
return
|
|
}
|
|
|
|
if (!invitingProfile) {
|
|
toast.error('Inviting profile not found')
|
|
localStorage.removeItem('inviteCode')
|
|
navigate('/')
|
|
return
|
|
}
|
|
|
|
setInvitingProfile(invitingProfile)
|
|
}
|
|
|
|
if (!id) throw new Error('Invite ID is required')
|
|
|
|
if (!isAuthenticationInitialized) return
|
|
|
|
if (isValidationDone) return
|
|
|
|
if (!isAuthenticated) {
|
|
// Save invite code in local storage
|
|
localStorage.setItem('inviteCode', id)
|
|
}
|
|
|
|
if (!isUserProfileLayerLoaded) return
|
|
|
|
setValidationDone(true)
|
|
void validateInvite(id)
|
|
}, [
|
|
id,
|
|
isAuthenticated,
|
|
inviteApi,
|
|
navigate,
|
|
isAuthenticationInitialized,
|
|
myProfile,
|
|
itemsApi,
|
|
isRedeemingDone,
|
|
isValidationDone,
|
|
createEmptyProfile,
|
|
isUserProfileLayerLoaded,
|
|
])
|
|
|
|
const goToSignup = () => {
|
|
navigate('/signup')
|
|
}
|
|
|
|
const goToLogin = () => {
|
|
navigate('/login')
|
|
}
|
|
|
|
const goToStart = () => {
|
|
navigate('/')
|
|
}
|
|
|
|
if (isAuthenticated) {
|
|
return (
|
|
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
|
|
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Confirmation</h2>
|
|
{invitingProfile ? (
|
|
<div className='tw-text-center tw-mb-4'>
|
|
<p className='tw-text-sm tw-text-gray-600'>
|
|
Do you want to follow <strong>{invitingProfile.name}</strong>?
|
|
</p>
|
|
<div className='tw-flex tw:justify-center tw:mt-4'>
|
|
<button className='tw-btn tw-btn-primary' onClick={confirmFollow}>
|
|
Yes
|
|
</button>
|
|
<button className='tw-btn tw-btn-secondary' onClick={goToStart}>
|
|
No
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className='tw-text-center'>Validating invite...</p>
|
|
)}
|
|
</MapOverlayPage>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
|
|
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Invitation</h2>
|
|
{invitingProfile ? (
|
|
<div className='tw-text-center tw-mb-4'>
|
|
<p className='tw-text-lg tw-font-semibold'>Welcome to Utopia!</p>
|
|
<p className='tw-text-sm tw-text-gray-600'>
|
|
You have been invited by: <strong>{invitingProfile.name}</strong> to join the Utopia
|
|
community.
|
|
</p>
|
|
<div className='tw-flex tw:justify-center tw:mt-4'>
|
|
<button className='tw-btn tw-btn-primary' onClick={goToSignup}>
|
|
{'Sign Up'}
|
|
</button>
|
|
<button className='tw-btn tw-btn-secondary' onClick={goToLogin}>
|
|
Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className='tw-text-center'>Validating invite...</p>
|
|
)}
|
|
</MapOverlayPage>
|
|
)
|
|
}
|