Added better auth. Still fixing issues
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"@tanstack/react-router": "^1.168.10",
|
||||
"@tanstack/router-devtools": "^1.166.11",
|
||||
"axios": "^1.14.0",
|
||||
"better-auth": "^1.6.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
@@ -62,13 +62,48 @@ export function OnboardingCompleteStep({
|
||||
<Input
|
||||
id="physical-address"
|
||||
type="text"
|
||||
placeholder="Ej: Av. San Martin 1234, Salta"
|
||||
placeholder="Ej: Av. San Martin 1234"
|
||||
aria-invalid={Boolean(form.formState.errors.physicalAddress)}
|
||||
{...form.register('physicalAddress')}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.physicalAddress]} />
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Field data-invalid={Boolean(form.formState.errors.city)}>
|
||||
<FieldLabel htmlFor="city">Ciudad</FieldLabel>
|
||||
<Input
|
||||
id="city"
|
||||
type="text"
|
||||
placeholder="Salta"
|
||||
aria-invalid={Boolean(form.formState.errors.city)}
|
||||
{...form.register('city')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.state)}>
|
||||
<FieldLabel htmlFor="state">Provincia</FieldLabel>
|
||||
<Input
|
||||
id="state"
|
||||
type="text"
|
||||
placeholder="Salta"
|
||||
aria-invalid={Boolean(form.formState.errors.state)}
|
||||
{...form.register('state')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.country)}>
|
||||
<FieldLabel htmlFor="country">Pais</FieldLabel>
|
||||
<Input
|
||||
id="country"
|
||||
type="text"
|
||||
placeholder="Argentina"
|
||||
aria-invalid={Boolean(form.formState.errors.country)}
|
||||
{...form.register('country')}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.planCode)}>
|
||||
<FieldLabel htmlFor="plan-code">Plan</FieldLabel>
|
||||
<Controller
|
||||
|
||||
@@ -24,6 +24,45 @@ import { useForm } from 'react-hook-form';
|
||||
const OTP_MAX_ATTEMPTS = 5;
|
||||
type OnboardStep = 'start' | 'verify-otp' | 'complete';
|
||||
|
||||
const ONBOARDING_STORAGE_KEY = 'playzer_onboarding_state';
|
||||
|
||||
type OnboardingStorageState = {
|
||||
step: OnboardStep;
|
||||
requestId: string | null;
|
||||
onboardingEmail: string | null;
|
||||
verifiedEmail: string | null;
|
||||
startFormValues?: { fullName: string; email: string };
|
||||
completeFormValues?: Partial<{
|
||||
password: string;
|
||||
complexName: string;
|
||||
physicalAddress: string;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
planCode: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
function loadOnboardingState(): OnboardingStorageState | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const stored = localStorage.getItem(ONBOARDING_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveOnboardingState(state: OnboardingStorageState) {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(ONBOARDING_STORAGE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
function clearOnboardingState() {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.removeItem(ONBOARDING_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function OnboardPage() {
|
||||
const navigate = useNavigate();
|
||||
const { signInWithPassword } = useAuth();
|
||||
@@ -39,6 +78,71 @@ export function OnboardPage() {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [infoMessage, setInfoMessage] = useState<string | null>(null);
|
||||
|
||||
// Load persisted state on mount
|
||||
useEffect(() => {
|
||||
const saved = loadOnboardingState();
|
||||
if (saved && saved.step !== 'start' && saved.requestId) {
|
||||
setStep(saved.step);
|
||||
setRequestId(saved.requestId);
|
||||
setOnboardingEmail(saved.onboardingEmail);
|
||||
setVerifiedEmail(saved.verifiedEmail);
|
||||
|
||||
if (saved.startFormValues) {
|
||||
startForm.reset(saved.startFormValues);
|
||||
}
|
||||
if (saved.completeFormValues) {
|
||||
completeForm.reset(saved.completeFormValues);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save state to localStorage when it changes
|
||||
useEffect(() => {
|
||||
const state: OnboardingStorageState = {
|
||||
step,
|
||||
requestId,
|
||||
onboardingEmail,
|
||||
verifiedEmail,
|
||||
startFormValues: startForm.getValues(),
|
||||
completeFormValues: step === 'complete' ? completeForm.getValues() : undefined,
|
||||
};
|
||||
|
||||
if (step !== 'start' && requestId) {
|
||||
saveOnboardingState(state);
|
||||
}
|
||||
}, [step, requestId, onboardingEmail, verifiedEmail]);
|
||||
|
||||
// Also save form values when they change
|
||||
useEffect(() => {
|
||||
if (step !== 'start' && requestId) {
|
||||
const state = loadOnboardingState();
|
||||
if (state) {
|
||||
saveOnboardingState({
|
||||
...state,
|
||||
startFormValues: startForm.getValues(),
|
||||
completeFormValues: step === 'complete' ? completeForm.getValues() : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [step, requestId]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (step !== 'start' && requestId) {
|
||||
const state = loadOnboardingState();
|
||||
if (state) {
|
||||
saveOnboardingState({
|
||||
...state,
|
||||
startFormValues: startForm.getValues(),
|
||||
completeFormValues: step === 'complete' ? completeForm.getValues() : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [step, requestId]);
|
||||
|
||||
const startForm = useForm<StartValues>({
|
||||
resolver: zodResolver(onboardingStartSchema),
|
||||
mode: 'onChange',
|
||||
@@ -55,6 +159,9 @@ export function OnboardPage() {
|
||||
password: '',
|
||||
complexName: '',
|
||||
physicalAddress: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: '',
|
||||
planCode: '',
|
||||
},
|
||||
});
|
||||
@@ -180,13 +287,27 @@ export function OnboardPage() {
|
||||
password: values.password,
|
||||
complexName: values.complexName,
|
||||
physicalAddress: values.physicalAddress,
|
||||
city: values.city,
|
||||
state: values.state,
|
||||
country: values.country,
|
||||
planCode: values.planCode,
|
||||
});
|
||||
setCurrentComplexSlug(result.complexSlug);
|
||||
clearOnboardingState();
|
||||
|
||||
const emailForSignIn = verifiedEmail ?? onboardingEmail;
|
||||
if (emailForSignIn) {
|
||||
await signInWithPassword({ email: emailForSignIn, password: values.password });
|
||||
try {
|
||||
await signInWithPassword({ email: emailForSignIn, password: values.password });
|
||||
} catch {
|
||||
await navigate({
|
||||
to: '/login',
|
||||
search: {
|
||||
redirect: `/complex/${result.complexSlug}/edit`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { setApiAccessToken } from '@/lib/api-client';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import type { Session, User } from '@supabase/supabase-js';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
import { emailOTPClient } from 'better-auth/client/plugins';
|
||||
import type { User, Session } from 'better-auth/types';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const authClient = createAuthClient({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000',
|
||||
plugins: [emailOTPClient()],
|
||||
});
|
||||
|
||||
type SignInParams = {
|
||||
email: string;
|
||||
@@ -18,7 +18,7 @@ type SignInParams = {
|
||||
type SignUpParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
fullName: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type UpdateProfileParams = {
|
||||
@@ -29,16 +29,19 @@ type UpdatePasswordParams = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
type AuthUser = User | null;
|
||||
type AuthSession = Session | null;
|
||||
|
||||
export type AuthContextValue = {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
user: AuthUser;
|
||||
session: AuthSession;
|
||||
loading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
displayName: string;
|
||||
initials: string;
|
||||
avatarUrl: string | null;
|
||||
signInWithPassword: (params: SignInParams) => Promise<void>;
|
||||
signUp: (params: SignUpParams) => Promise<{ requiresEmailConfirmation: boolean }>;
|
||||
signUp: (params: SignUpParams) => Promise<void>;
|
||||
updateProfile: (params: UpdateProfileParams) => Promise<void>;
|
||||
updatePassword: (params: UpdatePasswordParams) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
@@ -46,17 +49,11 @@ export type AuthContextValue = {
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
function getDisplayName(user: User | null): string {
|
||||
function getDisplayName(user: AuthUser): string {
|
||||
if (!user) return 'Usuario';
|
||||
|
||||
const fromMetadata =
|
||||
user.user_metadata?.name ?? user.user_metadata?.full_name ?? user.user_metadata?.user_name;
|
||||
|
||||
if (typeof fromMetadata === 'string' && fromMetadata.trim().length > 0) {
|
||||
return fromMetadata.trim();
|
||||
}
|
||||
|
||||
return user.email ?? 'Usuario';
|
||||
const name = user.name ?? user.email ?? 'Usuario';
|
||||
return name;
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
@@ -65,48 +62,30 @@ function getInitials(name: string): string {
|
||||
return initials || 'U';
|
||||
}
|
||||
|
||||
function getAvatarUrl(user: User | null): string | null {
|
||||
function getAvatarUrl(user: AuthUser): string | null {
|
||||
if (!user) return null;
|
||||
|
||||
const value = user.user_metadata?.avatar_url ?? user.user_metadata?.picture;
|
||||
return typeof value === 'string' && value.length > 0 ? value : null;
|
||||
return user.image ?? null;
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: PropsWithChildren) {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const sessionStore = authClient.useSession();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
setLoading(sessionStore.isPending);
|
||||
}, [sessionStore.isPending]);
|
||||
|
||||
const init = async () => {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const user = sessionStore.data?.user ?? null;
|
||||
const session = sessionStore.data?.session ?? null;
|
||||
const isAuthenticated = !!session;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setSession(data.session);
|
||||
setUser(data.session?.user ?? null);
|
||||
setApiAccessToken(data.session?.access_token ?? null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
void init();
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, nextSession) => {
|
||||
setSession(nextSession);
|
||||
setUser(nextSession?.user ?? null);
|
||||
setApiAccessToken(nextSession?.access_token ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (sessionStore.data?.session?.token) {
|
||||
setApiAccessToken(sessionStore.data.session.token);
|
||||
} else {
|
||||
setApiAccessToken(null);
|
||||
}
|
||||
}, [sessionStore.data?.session?.token]);
|
||||
|
||||
const value = useMemo<AuthContextValue>(() => {
|
||||
const displayName = getDisplayName(user);
|
||||
@@ -117,69 +96,58 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
user,
|
||||
session,
|
||||
loading,
|
||||
isAuthenticated: Boolean(user),
|
||||
isAuthenticated,
|
||||
displayName,
|
||||
initials,
|
||||
avatarUrl,
|
||||
signInWithPassword: async ({ email, password }) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
const result = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
},
|
||||
signUp: async ({ email, password, fullName }) => {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
signUp: async ({ email, password, name }) => {
|
||||
const result = await authClient.signUp.email({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
full_name: fullName,
|
||||
name: fullName,
|
||||
},
|
||||
},
|
||||
name,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
requiresEmailConfirmation: !data.session,
|
||||
};
|
||||
},
|
||||
updateProfile: async ({ fullName }) => {
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
data: {
|
||||
full_name: fullName,
|
||||
name: fullName,
|
||||
},
|
||||
const result = await (authClient as any).user.update({
|
||||
name: fullName,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (result?.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
},
|
||||
updatePassword: async ({ password }) => {
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
const result = await (authClient as any).user.update({
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (result?.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
},
|
||||
signOut: async () => {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
throw error;
|
||||
const result = await authClient.signOut();
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [loading, session, user]);
|
||||
}, [loading, session, user, isAuthenticated, sessionStore]);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
@@ -193,3 +161,5 @@ export function useAuth() {
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export { authClient };
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error(
|
||||
'Missing Supabase environment variables. Define VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.'
|
||||
);
|
||||
}
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
});
|
||||
@@ -3,4 +3,4 @@ import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/onboard')({
|
||||
component: OnboardPage,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user