So I can create the avatar no problem. I have stored the avatar/model id in Firebase.
But I am having issues signing in and then editing/customising my existing avatar.
I get you are not allowed to view that avatar.
What am I doing wrong? Anyone to help please? NextJS
Here’s the code I’m using
‘use client’;
import { useEffect, useMemo, useRef, useState } from ‘react’;
import { createPortal } from ‘react-dom’;
import { type User, updateProfile } from ‘firebase/auth’;
import { Button } from ‘./ui/button’;
import { Input } from ‘./ui/input’;
import { Label } from ‘./ui/label’;
import { Avatar, AvatarFallback, AvatarImage } from ‘./ui/avatar’;
import { Loader2, Sparkles, User as UserIcon } from ‘lucide-react’;
import { useToast } from ‘@/hooks/use-toast’;
import { auth } from ‘@/lib/firebase’;
interface ProfileSetupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
user: User;
}
const READY_PLAYER_ME_URL = ‘fake link for obvious reasons’’; //just studio name in here of the url
export function ProfileSetupDialog({ open, onOpenChange, user }: ProfileSetupDialogProps) {
const { toast } = useToast();
const [displayName, setDisplayName] = useState(‘’);
const [avatarUrl, setAvatarUrl] = useState(‘’);
const [saving, setSaving] = useState(false);
const iframeRef = useRef(null);
useEffect(() => {
if (!open) {
return;
}
setDisplayName(user.displayName ?? '');
setAvatarUrl(user.photoURL ?? '');
}, [open, user.displayName, user.photoURL]);
const previewAvatar = avatarUrl || user.photoURL || ‘’;
const initials = useMemo(() => {
if (displayName?.trim()) {
return displayName
.trim()
.split(/\\s+/)
.map((part) => part.charAt(0).toUpperCase())
.slice(0, 2)
.join('');
}
if (user.email) {
return user.email.charAt(0).toUpperCase();
}
return 'U';
}, [displayName, user.email]);
useEffect(() => {
if (!open) {
return;
}
const handleMessage = (event: MessageEvent) => {
let data: any = event.data;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (error) {
return;
}
}
if (data?.source !== 'readyplayerme') {
return;
}
if (data.eventName === 'v1.avatar.exported') {
const url = data.data?.url as string | undefined;
if (url) {
setAvatarUrl(url);
toast({
title: 'Avatar ready',
description: 'We swapped in your new look so you can preview it instantly.',
});
}
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [open, toast]);
useEffect(() => {
if (!open) {
return;
}
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalOverflow;
};
}, [open]);
const subscribeToEvents = () => {
const frameWindow = iframeRef.current?.contentWindow;
if (!frameWindow) {
return;
}
const subscribe = (eventName: string) =>
frameWindow.postMessage(
JSON.stringify({
target: 'readyplayerme',
type: 'subscribe',
eventName,
}),
'\*',
);
subscribe('v1.frame.ready');
subscribe('v1.avatar.exported');
};
const handleSave = async () => {
const trimmedName = displayName.trim();
if (!trimmedName) {
toast({
title: 'Display name required',
description: 'Please choose a display name to continue.',
variant: 'destructive',
});
return;
}
setSaving(true);
try {
await updateProfile(user, {
displayName: trimmedName,
photoURL: avatarUrl.trim() || null,
});
await user.reload();
await auth.currentUser?.reload();
toast({
title: 'Profile updated',
description: 'Your display name and avatar have been saved.',
});
onOpenChange(false);
} catch (error) {
console.error('Error updating profile', error);
toast({
title: 'Update failed',
description: 'We could not update your profile. Please try again.',
variant: 'destructive',
});
} finally {
setSaving(false);
}
};
if (!open) {
return null;
}
return createPortal(
<div className="fixed inset-0 z-50 flex h-full w-full text-white">
<div className="relative flex h-full w-full overflow-hidden bg-slate-950">
<div className="pointer-events-none absolute inset-0">
<div className="absolute left-1/2 top-\[-10%\] h-\[120%\] w-\[120%\] -translate-x-1/2 rounded-full bg-\[radial-gradient(circle_at_center,\_rgba(56,189,248,0.18),\_transparent_60%)\]" />
<div className="absolute inset-0 bg-\[url('/textures/asphalt.png')\] opacity-10 mix-blend-screen" />
<div className="absolute bottom-0 left-0 right-0 h-40 bg-gradient-to-t from-slate-950 via-slate-950/40 to-transparent" />
</div>
<aside className="relative z-10 flex w-full flex-col gap-8 border-b border-white/10 bg-slate-950/80 p-6 backdrop-blur-xl lg:w-\[420px\] lg:border-b-0 lg:border-r">
<div className="flex items-center justify-between">
<div className="inline-flex items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary">
<Sparkles className="h-3.5 w-3.5" />
Driver Garage
</div>
<Button
type="button"
variant="ghost"
className="text-slate-300 hover:text-white"
onClick={() => onOpenChange(false)}
disabled={saving}
>
Skip for now
</Button>
</div>
<header className="space-y-3">
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Create Your In-Game Persona
</h1>
<p className="text-sm text-slate-300 sm:text-base">
Dial in your call sign and suit before you roll onto the grid. We'll showcase your look on leaderboards, rival
challenges, and post-race celebrations.
</p>
</header>
<div className="rounded-2xl border border-white/10 bg-slate-900/70 p-6 shadow-xl">
<div className="flex items-center justify-between">
<span className="text-xs uppercase tracking-\[0.3em\] text-slate-400">Preview</span>
<span className="text-xs font-medium text-primary/80">Live update</span>
</div>
<div className="mt-4 flex flex-col items-center gap-4">
<div className="relative flex h-48 w-full items-center justify-center overflow-hidden rounded-xl border border-white/10 bg-gradient-to-br from-slate-900 via-slate-900/60 to-slate-800">
<div className="absolute inset-x-6 bottom-6 h-24 rounded-full bg-primary/20 blur-3xl" />
<Avatar className="relative z-10 h-40 w-40 border-2 border-primary/50 bg-slate-900">
<AvatarImage src={previewAvatar || undefined} alt="Avatar preview" />
<AvatarFallback className="bg-slate-800 text-3xl font-semibold text-white">
{initials || <UserIcon className="h-8 w-8" />}
</AvatarFallback>
</Avatar>
</div>
<div className="w-full space-y-2">
<Label htmlFor="display-name" className="text-xs uppercase tracking-wide text-slate-400">
Driver tag
</Label>
<Input
id="display-name"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
placeholder="e.g. ApexAce or TurboTina"
maxLength={40}
className="bg-slate-950/60 text-base"
/>
<p className="text-xs text-slate-400">
Your driver tag is shown in multiplayer lobbies, lap charts, and highlight reels.
</p>
</div>
{avatarUrl && (
<Button
type="button"
variant="ghost"
className="w-full justify-center text-sm text-slate-300 hover:text-white"
onClick={() => setAvatarUrl('')}
disabled={saving}
>
Reset to default avatar
</Button>
)}
</div>
</div>
<div className="flex flex-col gap-3 text-xs text-slate-400">
<p>
Finish customizing on the right and hit <span className="font-semibold text-slate-200">Export Avatar</span>. We'll drop the fresh render straight into your preview.
</p>
<p>
Ready Player Me might ask for camera access to generate your likeness. It's optional—you can always sculpt manually.
</p>
</div>
<div className="mt-auto flex flex-col gap-3 sm:flex-row">
<Button
type="button"
variant="secondary"
onClick={handleSave}
disabled={saving}
className="gap-2 bg-primary/20 text-primary hover:bg-primary/30"
>
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
Save & Hit the Track
</Button>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={saving}
className="border-white/20 text-slate-200 hover:bg-slate-800/60"
>
I'll tune it later
</Button>
</div>
</aside>
<main className="relative z-10 flex flex-1 flex-col bg-black/80">
<div className="hidden h-16 items-center justify-between border-b border-white/10 px-8 lg:flex">
<div className="space-y-1">
<p className="text-xs uppercase tracking-\[0.4em\] text-primary/70">Avatar Workshop</p>
<p className="text-sm text-slate-300">
Craft a driver that looks right at home in the pit lane.
</p>
</div>
</div>
<div className="flex items-center justify-between border-b border-white/10 px-4 py-3 lg:hidden">
<div className="space-y-1">
<p className="text-xs uppercase tracking-\[0.4em\] text-primary/70">Avatar Workshop</p>
<p className="text-xs text-slate-300">
Use the controls below to export your driver.
</p>
</div>
</div>
<iframe
ref={iframeRef}
src={READY_PLAYER_ME_URL}
title="Driver Persona Customiser"
className="h-full w-full lg:border-l lg:border-white/10"
allow="camera \*; microphone \*"
onLoad={subscribeToEvents}
/>
</main>
</div>
</div>,
document.body,
);
}