From 68fb2019075ce33232f27b09068b3e3de2631aeb Mon Sep 17 00:00:00 2001 From: Maximilian Harz Date: Thu, 4 Dec 2025 00:13:33 +0100 Subject: [PATCH] Show dialog to accept following --- lib/src/Components/Map/UtopiaMap.tsx | 6 +- lib/src/Components/Map/hooks/useMyProfile.ts | 33 +++++- .../Components/Map/hooks/useRedeemInvite.ts | 35 ------ .../Map/hooks/useStoredInviteCode.ts | 17 +++ lib/src/Components/Onboarding/InvitePage.tsx | 108 ++++++++++++------ 5 files changed, 124 insertions(+), 75 deletions(-) delete mode 100644 lib/src/Components/Map/hooks/useRedeemInvite.ts create mode 100644 lib/src/Components/Map/hooks/useStoredInviteCode.ts diff --git a/lib/src/Components/Map/UtopiaMap.tsx b/lib/src/Components/Map/UtopiaMap.tsx index 93f933ca..dee84b1f 100644 --- a/lib/src/Components/Map/UtopiaMap.tsx +++ b/lib/src/Components/Map/UtopiaMap.tsx @@ -3,7 +3,7 @@ import { MapContainer } from 'react-leaflet' import { ContextWrapper } from '#components/AppShell/ContextWrapper' -import { useRedeemInvite } from './hooks/useRedeemInvite' +import { useStoredInviteCode } from './hooks/useStoredInviteCode' import { UtopiaMapInner } from './UtopiaMapInner' import type { InviteApi } from '#types/InviteApi' @@ -112,8 +112,8 @@ function UtopiaMap({ /** tile size (default 256) */ tileSize?: number }) { - // Check for invite code in localStorage and loaded profile, redeem if possible - useRedeemInvite(inviteApi) + // Check for invite code in localStorage + useStoredInviteCode() return ( { const items = useItems() const { user } = useAuth() + const layers = useLayers() + const addItem = useAddItem() // Find the user's profile item const myProfile = items.find( @@ -16,5 +19,31 @@ export const useMyProfile = () => { // allItemsLoaded is not reliable const isMyProfileLoaded = isAnyUserProfileLoaded && !!user - return { myProfile, isMyProfileLoaded } + const createEmptyProfile = async () => { + if (!user) return + + const userLayer = layers.find((l) => l.userProfileLayer === true) + if (!userLayer?.api?.createItem) { + throw new Error('User profile layer or create API not available') + } + + const newProfile = { + id: crypto.randomUUID(), + name: user.first_name ?? 'User', + } + + const result = await userLayer.api.createItem(newProfile) + + // Use server response for local state update + addItem({ + ...result, + user_created: user, + layer: userLayer, + public_edit: false, + }) + + return result + } + + return { myProfile, isMyProfileLoaded, createEmptyProfile } } diff --git a/lib/src/Components/Map/hooks/useRedeemInvite.ts b/lib/src/Components/Map/hooks/useRedeemInvite.ts deleted file mode 100644 index 7a403848..00000000 --- a/lib/src/Components/Map/hooks/useRedeemInvite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { toast } from 'react-toastify' - -import { useMyProfile } from './useMyProfile' - -import type { InviteApi } from '#types/InviteApi' -import type { Item } from '#types/Item' - -export const useRedeemInvite = (inviteApi: InviteApi) => { - const inviteCode = localStorage.getItem('inviteCode') - - const { myProfile } = useMyProfile() - const navigate = useNavigate() - - const [isRedeemingDone, setRedeemingDone] = useState(false) - - useEffect(() => { - async function redeemInvite(inviteCode: string, myProfile: Item) { - const invitingProfileId = await inviteApi.redeemInvite(inviteCode, myProfile.id) - - if (invitingProfileId) { - toast.success('Invite redeemed successfully!') - localStorage.removeItem('inviteCode') - navigate(`/item/${invitingProfileId}`) - } else { - toast.error('Failed to redeem invite') - } - } - - if (!inviteCode || !myProfile || isRedeemingDone) return - void redeemInvite(inviteCode, myProfile) - setRedeemingDone(true) - }, [inviteApi, inviteCode, isRedeemingDone, myProfile, navigate]) -} diff --git a/lib/src/Components/Map/hooks/useStoredInviteCode.ts b/lib/src/Components/Map/hooks/useStoredInviteCode.ts new file mode 100644 index 00000000..fcd9b6f8 --- /dev/null +++ b/lib/src/Components/Map/hooks/useStoredInviteCode.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' + +import { useAuth } from '#components/Auth/useAuth' + +export const useStoredInviteCode = () => { + const inviteCode = localStorage.getItem('inviteCode') + + const { user } = useAuth() + const navigate = useNavigate() + + useEffect(() => { + if (!inviteCode || !user) return + + navigate(`/invite/${inviteCode}`) + }, [inviteCode, navigate, user]) +} diff --git a/lib/src/Components/Onboarding/InvitePage.tsx b/lib/src/Components/Onboarding/InvitePage.tsx index 0e9d04a5..0f67242a 100644 --- a/lib/src/Components/Onboarding/InvitePage.tsx +++ b/lib/src/Components/Onboarding/InvitePage.tsx @@ -23,7 +23,7 @@ export function InvitePage({ inviteApi, itemsApi }: Props) { const { id } = useParams<{ id: string }>() const navigate = useNavigate() - const { myProfile, isMyProfileLoaded } = useMyProfile() + const { myProfile, isMyProfileLoaded, createEmptyProfile } = useMyProfile() if (!id) throw new Error('Invite ID is required') @@ -31,20 +31,42 @@ export function InvitePage({ inviteApi, itemsApi }: Props) { const [isRedeemingDone, setRedeemingDone] = useState(false) const [isValidationDone, setValidationDone] = useState(false) - useEffect(() => { - async function redeemInvite(id: string, myProfileId: string) { - const invitingProfileId = await inviteApi.redeemInvite(id, myProfileId) + async function redeemInvite(id: string, myProfileId: string) { + const invitingProfileId = await inviteApi.redeemInvite(id, myProfileId) - if (invitingProfileId) { - toast.success('Invite redeemed successfully!') - setRedeemingDone(true) - navigate(`/item/${invitingProfileId}`) - } else { - toast.error('Failed to redeem invite') - navigate('/') - } + 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 (!isMyProfileLoaded || isRedeemingDone) return + + const myActualProfile = myProfile ?? (await createEmptyProfile()) + + if (!myActualProfile) { + toast.error('Failed to create profile') + return } + await redeemInvite(id, myActualProfile.id) + + setRedeemingDone(true) + } + + const confirmFollow = () => { + void confirmFollowAsync() + } + + useEffect(() => { async function validateInvite(id: string) { const invitingProfileId = await inviteApi.validateInvite(id) @@ -72,23 +94,14 @@ export function InvitePage({ inviteApi, itemsApi }: Props) { if (!isAuthenticationInitialized) return - if (isAuthenticated) { - if (!isMyProfileLoaded || isRedeemingDone) return - - if (!myProfile) { - toast.error('Could not find your profile to redeem the invite.') - } else { - void redeemInvite(id, myProfile.id) - } - setRedeemingDone(true) - } else { - if (isValidationDone) return + if (isValidationDone) return + if (!isAuthenticated) { // Save invite code in local storage localStorage.setItem('inviteCode', id) - - void validateInvite(id) } + + void validateInvite(id) }, [ id, isAuthenticated, @@ -100,6 +113,7 @@ export function InvitePage({ inviteApi, itemsApi }: Props) { itemsApi, isRedeemingDone, isValidationDone, + createEmptyProfile, ]) const goToSignup = () => { @@ -110,6 +124,35 @@ export function InvitePage({ inviteApi, itemsApi }: Props) { navigate('/login') } + const goToStart = () => { + navigate('/') + } + + if (isAuthenticated) { + return ( + +

Confirmation

+ {invitingProfile ? ( +
+

+ Do you want to follow {invitingProfile.name}? +

+
+ + +
+
+ ) : ( +

Validating invite...

+ )} +
+ ) + } + return (

Invitation

@@ -121,17 +164,12 @@ export function InvitePage({ inviteApi, itemsApi }: Props) { community.

- + - {!isAuthenticated && ( - - )}
) : (