UNPKG

@digilogiclabs/saas-factory-auth

Version:

Modern authentication package for Next.js 15+ and React 18.2+/19+ applications with React Native 0.72+ support using Supabase and Firebase

1,482 lines (1,472 loc) 51.8 kB
import { useRef, useEffect } from 'react'; import { createClient } from '@supabase/supabase-js'; import { initializeApp } from 'firebase/app'; import { getAuth, setPersistence, browserLocalPersistence, signInWithEmailAndPassword, createUserWithEmailAndPassword, signInWithPopup, sendSignInLinkToEmail, signOut as signOut$1, sendPasswordResetEmail, onAuthStateChanged, updateProfile as updateProfile$1, OAuthProvider, TwitterAuthProvider, FacebookAuthProvider, GithubAuthProvider, GoogleAuthProvider } from 'firebase/auth'; import { create } from 'zustand'; import { devtools, persist, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { jsx, Fragment } from 'react/jsx-runtime'; import { createBrowserClient, createServerClient } from '@supabase/ssr'; import { NextResponse } from 'next/server.js'; import 'next/headers.js'; // src/components/AuthProvider.tsx // src/core/IAuthProvider.ts var AuthError = class _AuthError extends Error { constructor(type, message, originalError, context) { super(message); this.type = type; this.originalError = originalError; this.context = context; this.name = "AuthError"; this.timestamp = /* @__PURE__ */ new Date(); this.code = type; Object.setPrototypeOf(this, _AuthError.prototype); if (Error.captureStackTrace) { Error.captureStackTrace(this, _AuthError); } } timestamp; code; /** * Convert error to JSON for logging/debugging */ toJSON() { const originalErrorDetails = this.originalError instanceof Error ? { name: this.originalError.name, message: this.originalError.message, stack: this.originalError.stack } : this.originalError ? { message: String(this.originalError) } : void 0; return { name: this.name, type: this.type, code: this.code, message: this.message, timestamp: this.timestamp.toISOString(), context: this.context, originalError: originalErrorDetails }; } /** * Check if error is of a specific type */ isType(type) { return this.type === type; } /** * Check if error is retryable */ isRetryable() { return [ "NETWORK_ERROR" /* NETWORK_ERROR */, "PROVIDER_ERROR" /* PROVIDER_ERROR */ ].includes(this.type); } }; // src/config/env.ts var getAuthConfig = () => { const config = {}; if (typeof process !== "undefined" && process.env) { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY; const firebaseApiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY || process.env.FIREBASE_API_KEY; const firebaseAuthDomain = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || process.env.FIREBASE_AUTH_DOMAIN; const firebaseProjectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID || process.env.FIREBASE_PROJECT_ID; if (supabaseUrl) config.supabaseUrl = supabaseUrl; if (supabaseAnonKey) config.supabaseAnonKey = supabaseAnonKey; if (firebaseApiKey) config.firebaseApiKey = firebaseApiKey; if (firebaseAuthDomain) config.firebaseAuthDomain = firebaseAuthDomain; if (firebaseProjectId) config.firebaseProjectId = firebaseProjectId; return config; } try { const Constants = new Function("return require('expo-constants').default")(); const expoConfig = Constants.expoConfig?.extra || {}; if (expoConfig.supabaseUrl) config.supabaseUrl = expoConfig.supabaseUrl; if (expoConfig.supabaseAnonKey) config.supabaseAnonKey = expoConfig.supabaseAnonKey; if (expoConfig.firebaseApiKey) config.firebaseApiKey = expoConfig.firebaseApiKey; if (expoConfig.firebaseAuthDomain) config.firebaseAuthDomain = expoConfig.firebaseAuthDomain; if (expoConfig.firebaseProjectId) config.firebaseProjectId = expoConfig.firebaseProjectId; return config; } catch { } try { const RNConfig = new Function("return require('react-native-config').default")(); if (RNConfig.SUPABASE_URL) config.supabaseUrl = RNConfig.SUPABASE_URL; if (RNConfig.SUPABASE_ANON_KEY) config.supabaseAnonKey = RNConfig.SUPABASE_ANON_KEY; if (RNConfig.FIREBASE_API_KEY) config.firebaseApiKey = RNConfig.FIREBASE_API_KEY; if (RNConfig.FIREBASE_AUTH_DOMAIN) config.firebaseAuthDomain = RNConfig.FIREBASE_AUTH_DOMAIN; if (RNConfig.FIREBASE_PROJECT_ID) config.firebaseProjectId = RNConfig.FIREBASE_PROJECT_ID; return config; } catch { } return {}; }; var validateConfig = (config, provider) => { if (provider === "supabase") { if (!config.supabaseUrl || !config.supabaseAnonKey) { throw new Error( "Supabase configuration is incomplete. Please ensure NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set." ); } } if (provider === "firebase") { if (!config.firebaseApiKey || !config.firebaseAuthDomain || !config.firebaseProjectId) { throw new Error( "Firebase configuration is incomplete. Please ensure NEXT_PUBLIC_FIREBASE_API_KEY, NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, and NEXT_PUBLIC_FIREBASE_PROJECT_ID are set." ); } } }; // src/types.ts var AuthEvent = /* @__PURE__ */ ((AuthEvent2) => { AuthEvent2["SIGNED_IN"] = "SIGNED_IN"; AuthEvent2["SIGNED_OUT"] = "SIGNED_OUT"; AuthEvent2["TOKEN_REFRESHED"] = "TOKEN_REFRESHED"; AuthEvent2["USER_UPDATED"] = "USER_UPDATED"; return AuthEvent2; })(AuthEvent || {}); // src/core/SupabaseAuthProvider.ts var mapSupabaseUserToUser = (supabaseUser) => { if (!supabaseUser) return null; const metadata = supabaseUser.user_metadata || {}; return { id: supabaseUser.id, email: supabaseUser.email, emailVerified: supabaseUser.email_confirmed_at ? true : false, // Name fields with fallback logic name: metadata.name || metadata.full_name || metadata.display_name || null, firstName: metadata.firstName || metadata.first_name || null, lastName: metadata.lastName || metadata.last_name || null, fullName: metadata.fullName || metadata.full_name || metadata.name || null, // Profile fields avatar: metadata.avatar || metadata.avatar_url || metadata.picture || null, phone: metadata.phone || metadata.phone_number || null, website: metadata.website || null, bio: metadata.bio || null, // Additional metadata locale: metadata.locale || null, timezone: metadata.timezone || null, dateOfBirth: metadata.dateOfBirth || metadata.date_of_birth || null, // Timestamps createdAt: supabaseUser.created_at, updatedAt: supabaseUser.updated_at, lastSignInAt: supabaseUser.last_sign_in_at, // OAuth provider data providerData: supabaseUser.identities?.reduce((acc, identity) => { if (identity.provider && identity.identity_data) { acc[identity.provider] = { id: identity.id, username: identity.identity_data.user_name || identity.identity_data.login || null, raw: identity.identity_data }; } return acc; }, {}) || {}, // Include all other metadata for backward compatibility ...metadata }; }; var mapSupabaseError = (error) => { if (error instanceof AuthError) { return error; } if (typeof error !== "object" || error === null || !("message" in error)) { return new AuthError("UNKNOWN" /* UNKNOWN */, "An unknown error occurred", error); } const err = error; const message = err.message || "Unknown error occurred"; const code = err.code; if (code === "ERR_NAME_NOT_RESOLVED" || message.includes("ERR_NAME_NOT_RESOLVED")) { return new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, "Invalid Supabase URL. Please check your NEXT_PUBLIC_SUPABASE_URL configuration.", err ); } if (message.includes("Failed to fetch") || message.includes("NetworkError")) { return new AuthError( "NETWORK_ERROR" /* NETWORK_ERROR */, "Network error. Please check your internet connection and Supabase URL.", err ); } if (message.includes("Invalid login credentials")) { return new AuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, message, err); } if (message.includes("User not found")) { return new AuthError("USER_NOT_FOUND" /* USER_NOT_FOUND */, message, err); } if (message.includes("User already registered")) { return new AuthError("EMAIL_ALREADY_EXISTS" /* EMAIL_ALREADY_EXISTS */, message, err); } if (message.includes("Password should be")) { return new AuthError("WEAK_PASSWORD" /* WEAK_PASSWORD */, message, err); } if (message.includes("network") || message.includes("fetch")) { return new AuthError("NETWORK_ERROR" /* NETWORK_ERROR */, message, err); } return new AuthError("PROVIDER_ERROR" /* PROVIDER_ERROR */, message, err); }; var validateEmail = (email) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new AuthError("VALIDATION_ERROR" /* VALIDATION_ERROR */, "Invalid email format"); } }; var validateSupabaseUrl = (url) => { if (!url || !url.startsWith("http")) { throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, "Supabase URL must start with http:// or https://" ); } const urlObj = new URL(url); if (!urlObj.hostname.endsWith(".supabase.co") && !urlObj.hostname.includes("localhost")) { console.warn("Warning: URL does not appear to be a valid Supabase URL"); } if (url.includes("supabase.co/") && !url.includes(".supabase.co")) { throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, "Invalid Supabase URL format. Should be: https://[project-ref].supabase.co" ); } }; var SupabaseAuthProvider = class { client; storage; constructor(storage) { this.storage = storage; const config = getAuthConfig(); validateConfig(config, "supabase"); try { validateSupabaseUrl(config.supabaseUrl); } catch (error) { console.error("Supabase URL validation failed:", error); throw error; } try { this.client = createClient(config.supabaseUrl, config.supabaseAnonKey, { auth: { storage: { getItem: (key) => this.storage.getItem(key), setItem: (key, value) => this.storage.setItem(key, value), removeItem: (key) => this.storage.removeItem(key) }, autoRefreshToken: true, persistSession: true, detectSessionInUrl: true } }); } catch (error) { console.error("Failed to create Supabase client:", error); throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, "Failed to initialize Supabase client. Please check your configuration.", error ); } } /** * Validates Supabase configuration */ async validateConfiguration() { try { const config = getAuthConfig(); validateConfig(config, "supabase"); validateSupabaseUrl(config.supabaseUrl); const { error } = await this.client.auth.getSession(); if (error && error.message.includes("ERR_NAME_NOT_RESOLVED")) { throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, "Cannot connect to Supabase. Please verify your NEXT_PUBLIC_SUPABASE_URL is correct.", error ); } } catch (error) { if (error instanceof AuthError) { throw error; } throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, error instanceof Error ? error.message : "Invalid Supabase configuration" ); } } async signIn(email, password) { try { validateEmail(email); const { data, error } = await this.client.auth.signInWithPassword({ email, password }); if (error) { return { user: null, session: null, error: mapSupabaseError(error) }; } const user = mapSupabaseUserToUser(data.user); const session = data.session ? { user, access_token: data.session.access_token, refresh_token: data.session.refresh_token, expires_at: data.session.expires_at } : null; return { user, session, error: null }; } catch (error) { return { user: null, session: null, error: mapSupabaseError(error) }; } } async signUp(email, password) { try { validateEmail(email); const { data, error } = await this.client.auth.signUp({ email, password }); if (error) { return { user: null, session: null, error: mapSupabaseError(error) }; } const user = mapSupabaseUserToUser(data.user); const session = data.session ? { user, access_token: data.session.access_token, refresh_token: data.session.refresh_token, expires_at: data.session.expires_at } : null; return { user, session, error: null }; } catch (error) { return { user: null, session: null, error: mapSupabaseError(error) }; } } async signInWithOAuth(provider, redirectTo) { try { const options = {}; if (redirectTo) { options.redirectTo = redirectTo; } let supabaseProvider = provider; switch (provider) { case "microsoft": supabaseProvider = "azure"; break; case "apple": case "google": case "github": case "facebook": case "twitter": case "discord": case "linkedin": case "spotify": case "twitch": case "slack": case "notion": supabaseProvider = provider; break; default: throw new AuthError( "PROVIDER_ERROR" /* PROVIDER_ERROR */, `OAuth provider '${provider}' is not supported by Supabase` ); } const { error } = await this.client.auth.signInWithOAuth({ provider: supabaseProvider, // Type assertion needed due to Supabase's strict typing options }); if (error) { throw mapSupabaseError(error); } } catch (error) { throw mapSupabaseError(error); } } async signInWithMagicLink(email, redirectTo) { try { validateEmail(email); const options = {}; if (redirectTo) { options.emailRedirectTo = redirectTo; } const { error } = await this.client.auth.signInWithOtp({ email, options }); if (error) { throw mapSupabaseError(error); } } catch (error) { throw mapSupabaseError(error); } } async signOut() { try { const { error } = await this.client.auth.signOut(); if (error) { return { error: mapSupabaseError(error) }; } return { error: null }; } catch (error) { return { error: mapSupabaseError(error) }; } } async getUser() { try { const { data: { user }, error } = await this.client.auth.getUser(); if (error) { throw mapSupabaseError(error); } return mapSupabaseUserToUser(user); } catch (error) { throw mapSupabaseError(error); } } async getSession() { try { const { data: { session }, error } = await this.client.auth.getSession(); if (error) { if (error.message === "Auth session missing!") { return null; } throw mapSupabaseError(error); } if (!session) return null; const user = mapSupabaseUserToUser(session.user); return { user, access_token: session.access_token, refresh_token: session.refresh_token, expires_at: session.expires_at }; } catch (error) { if (error instanceof AuthError) { throw error; } throw new AuthError("PROVIDER_ERROR" /* PROVIDER_ERROR */, "Get session failed", error); } } onAuthStateChange(callback) { const { data: { subscription } } = this.client.auth.onAuthStateChange((event, session) => { let authEvent; switch (event) { case "SIGNED_IN": authEvent = "SIGNED_IN" /* SIGNED_IN */; break; case "SIGNED_OUT": authEvent = "SIGNED_OUT" /* SIGNED_OUT */; break; case "TOKEN_REFRESHED": authEvent = "TOKEN_REFRESHED" /* TOKEN_REFRESHED */; break; case "USER_UPDATED": authEvent = "USER_UPDATED" /* USER_UPDATED */; break; case "INITIAL_SESSION": authEvent = session ? "SIGNED_IN" /* SIGNED_IN */ : "SIGNED_OUT" /* SIGNED_OUT */; break; case "MFA_CHALLENGE_VERIFIED": authEvent = "USER_UPDATED" /* USER_UPDATED */; break; case "PASSWORD_RECOVERY": authEvent = "USER_UPDATED" /* USER_UPDATED */; break; default: console.warn(`Unhandled Supabase auth event: ${event}`); authEvent = "USER_UPDATED" /* USER_UPDATED */; } const user = mapSupabaseUserToUser(session?.user || null); const authSession = user && session ? { user, access_token: session.access_token, refresh_token: session.refresh_token, expires_at: session.expires_at } : null; callback(authEvent, authSession); }); return { unsubscribe: () => subscription.unsubscribe() }; } async resetPassword(email) { try { validateEmail(email); const { error } = await this.client.auth.resetPasswordForEmail(email); if (error) { return { error: mapSupabaseError(error) }; } return { error: null }; } catch (error) { return { error: mapSupabaseError(error) }; } } async updateProfile(updates) { try { const { data: { session }, error: sessionError } = await this.client.auth.getSession(); if (sessionError || !session?.user) { return { user: null, error: new AuthError( "INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "User must be authenticated to update profile" ) }; } const userMetadata = { ...session.user.user_metadata }; if (updates.name !== void 0) { userMetadata.name = updates.name; userMetadata.display_name = updates.name; } if (updates.firstName !== void 0) { userMetadata.firstName = updates.firstName; userMetadata.first_name = updates.firstName; } if (updates.lastName !== void 0) { userMetadata.lastName = updates.lastName; userMetadata.last_name = updates.lastName; } if (updates.fullName !== void 0) { userMetadata.fullName = updates.fullName; userMetadata.full_name = updates.fullName; userMetadata.name = updates.fullName; userMetadata.display_name = updates.fullName; } if ((updates.firstName !== void 0 || updates.lastName !== void 0) && updates.fullName === void 0) { const firstName = updates.firstName || userMetadata.firstName || userMetadata.first_name || ""; const lastName = updates.lastName || userMetadata.lastName || userMetadata.last_name || ""; const constructedFullName = `${firstName} ${lastName}`.trim(); if (constructedFullName) { userMetadata.full_name = constructedFullName; userMetadata.fullName = constructedFullName; userMetadata.name = constructedFullName; } } if (updates.avatar !== void 0) { userMetadata.avatar = updates.avatar; userMetadata.avatar_url = updates.avatar; userMetadata.picture = updates.avatar; } if (updates.phone !== void 0) { userMetadata.phone = updates.phone; userMetadata.phone_number = updates.phone; } if (updates.website !== void 0) { userMetadata.website = updates.website; } if (updates.bio !== void 0) { userMetadata.bio = updates.bio; } const knownFields = ["name", "firstName", "lastName", "fullName", "avatar", "phone", "website", "bio"]; Object.keys(updates).forEach((key) => { if (!knownFields.includes(key)) { userMetadata[key] = updates[key]; } }); const { data, error } = await this.client.auth.updateUser({ data: userMetadata }); if (error) { return { user: null, error: mapSupabaseError(error) }; } const updatedUser = mapSupabaseUserToUser(data.user); return { user: updatedUser, error: null }; } catch (error) { return { user: null, error: mapSupabaseError(error) }; } } }; var mapFirebaseUserToUser = (firebaseUser) => { if (!firebaseUser) return null; return { id: firebaseUser.uid, email: firebaseUser.email || void 0, name: firebaseUser.displayName, avatar: firebaseUser.photoURL, // Add other Firebase-specific metadata if needed phoneNumber: firebaseUser.phoneNumber, emailVerified: firebaseUser.emailVerified, isAnonymous: firebaseUser.isAnonymous, tenantId: firebaseUser.tenantId, providerData: firebaseUser.providerData, creationTime: firebaseUser.metadata?.creationTime, lastSignInTime: firebaseUser.metadata?.lastSignInTime }; }; var getFirebaseAccessToken = async (user) => { try { return await user.getIdToken(); } catch (error) { console.warn("Failed to get Firebase ID token:", error); return ""; } }; var mapFirebaseError = (error) => { const code = error.code || ""; const message = error.message || "Unknown error occurred"; switch (code) { case "auth/invalid-email": case "auth/user-disabled": case "auth/user-not-found": case "auth/wrong-password": case "auth/invalid-credential": return new AuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, message, error); case "auth/email-already-in-use": return new AuthError("EMAIL_ALREADY_EXISTS" /* EMAIL_ALREADY_EXISTS */, message, error); case "auth/weak-password": return new AuthError("WEAK_PASSWORD" /* WEAK_PASSWORD */, message, error); case "auth/network-request-failed": case "auth/timeout": return new AuthError("NETWORK_ERROR" /* NETWORK_ERROR */, message, error); case "auth/configuration-not-found": case "auth/invalid-api-key": return new AuthError("CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, message, error); default: return new AuthError("PROVIDER_ERROR" /* PROVIDER_ERROR */, message, error); } }; var validateEmail2 = (email) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new AuthError("VALIDATION_ERROR" /* VALIDATION_ERROR */, "Invalid email format"); } }; var getOAuthProvider = (provider) => { switch (provider) { case "google": return new GoogleAuthProvider(); case "github": return new GithubAuthProvider(); case "facebook": return new FacebookAuthProvider(); case "twitter": return new TwitterAuthProvider(); case "apple": { const appleProvider = new OAuthProvider("apple.com"); return appleProvider; } case "discord": { const discordProvider = new OAuthProvider("discord.com"); return discordProvider; } default: throw new AuthError( "VALIDATION_ERROR" /* VALIDATION_ERROR */, `Unsupported OAuth provider for Firebase: ${provider}` ); } }; var FirebaseAuthProvider = class { app; auth; storage; constructor(storage) { this.storage = storage; const config = getAuthConfig(); validateConfig(config, "firebase"); this.app = initializeApp({ apiKey: config.firebaseApiKey, authDomain: config.firebaseAuthDomain, projectId: config.firebaseProjectId }); this.auth = getAuth(this.app); setPersistence(this.auth, this.storage || browserLocalPersistence).catch((error) => { console.error("Failed to set Firebase auth persistence:", error); }); } /** * Validates Firebase configuration */ async validateConfiguration() { try { const config = getAuthConfig(); validateConfig(config, "firebase"); } catch (error) { throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, error instanceof Error ? error.message : "Invalid Firebase configuration" ); } } async signIn(email, password) { try { validateEmail2(email); const userCredential = await signInWithEmailAndPassword(this.auth, email, password); const user = mapFirebaseUserToUser(userCredential.user); const accessToken = await getFirebaseAccessToken(userCredential.user); const session = user ? { user, access_token: accessToken, refresh_token: void 0, // Firebase doesn't expose refresh token directly expires_at: void 0 // Firebase handles token expiration internally } : null; return { user, session, error: null }; } catch (error) { return { user: null, session: null, error: mapFirebaseError(error) }; } } async signUp(email, password) { try { validateEmail2(email); const userCredential = await createUserWithEmailAndPassword(this.auth, email, password); const user = mapFirebaseUserToUser(userCredential.user); const accessToken = await getFirebaseAccessToken(userCredential.user); const session = user ? { user, access_token: accessToken, refresh_token: void 0, expires_at: void 0 } : null; return { user, session, error: null }; } catch (error) { return { user: null, session: null, error: mapFirebaseError(error) }; } } async signInWithOAuth(provider, redirectTo) { try { const authProvider = getOAuthProvider(provider); if (redirectTo) { console.warn( "redirectTo is not directly supported for Firebase signInWithPopup. Consider using signInWithRedirect or handling redirects manually." ); } await signInWithPopup(this.auth, authProvider); } catch (error) { if (error instanceof AuthError) { throw error; } throw mapFirebaseError(error); } } async signInWithMagicLink(email, redirectTo) { try { validateEmail2(email); if (!redirectTo) { throw new AuthError( "VALIDATION_ERROR" /* VALIDATION_ERROR */, "redirectTo is required for Firebase magic link sign-in" ); } await sendSignInLinkToEmail(this.auth, email, { url: redirectTo, handleCodeInApp: true }); } catch (error) { if (error instanceof AuthError) { throw error; } throw mapFirebaseError(error); } } async signOut() { try { await signOut$1(this.auth); return { error: null }; } catch (error) { return { error: mapFirebaseError(error) }; } } async resetPassword(email) { try { validateEmail2(email); await sendPasswordResetEmail(this.auth, email); return { error: null }; } catch (error) { return { error: mapFirebaseError(error) }; } } async getUser() { return mapFirebaseUserToUser(this.auth.currentUser); } async getSession() { try { const currentUser = this.auth.currentUser; if (!currentUser) return null; const idTokenResult = await currentUser.getIdTokenResult(); const user = mapFirebaseUserToUser(currentUser); return { user, access_token: idTokenResult.token, refresh_token: void 0, // Firebase doesn't expose refresh token in IdTokenResult expires_at: idTokenResult.expirationTime ? new Date(idTokenResult.expirationTime).getTime() : void 0 }; } catch (error) { throw mapFirebaseError(error); } } onAuthStateChange(callback) { const unsubscribe = onAuthStateChanged(this.auth, async (user) => { const mappedUser = mapFirebaseUserToUser(user); let authEvent; let authSession = null; if (user) { authEvent = "SIGNED_IN" /* SIGNED_IN */; try { const idTokenResult = await user.getIdTokenResult(); authSession = { user: mappedUser, access_token: idTokenResult.token, refresh_token: void 0, // Firebase doesn't expose refresh token expires_at: idTokenResult.expirationTime ? new Date(idTokenResult.expirationTime).getTime() : void 0 }; } catch (error) { console.error("Error getting Firebase session in onAuthStateChange:", error); const accessToken = await getFirebaseAccessToken(user); authSession = { user: mappedUser, access_token: accessToken, refresh_token: void 0, expires_at: void 0 }; } } else { authEvent = "SIGNED_OUT" /* SIGNED_OUT */; } callback(authEvent, authSession); }); return { unsubscribe }; } async updateProfile(updates) { try { const currentUser = this.auth.currentUser; if (!currentUser) { return { user: null, error: new AuthError( "INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "User must be authenticated to update profile" ) }; } const profileUpdate = {}; if (updates.name !== void 0) { profileUpdate.displayName = updates.name; } else if (updates.fullName !== void 0) { profileUpdate.displayName = updates.fullName; } else if (updates.firstName || updates.lastName) { const firstName = updates.firstName || ""; const lastName = updates.lastName || ""; profileUpdate.displayName = `${firstName} ${lastName}`.trim() || null; } if (updates.avatar !== void 0) { profileUpdate.photoURL = updates.avatar; } if (Object.keys(profileUpdate).length > 0) { await updateProfile$1(currentUser, profileUpdate); } const unsupportedFields = Object.keys(updates).filter( (key) => !["name", "firstName", "lastName", "fullName", "avatar"].includes(key) ); if (unsupportedFields.length > 0) { console.warn( `Firebase updateProfile: The following fields are not natively supported by Firebase Auth and were ignored: ${unsupportedFields.join(", ")}. Consider storing extended user data in Firestore or Firebase Realtime Database.` ); } const updatedUser = mapFirebaseUserToUser(currentUser); return { user: updatedUser, error: null }; } catch (error) { return { user: null, error: mapFirebaseError(error) }; } } }; // src/config/storage.ts var BrowserStorage = class { async getItem(key) { try { return localStorage.getItem(key); } catch { console.warn("localStorage not available"); return null; } } async setItem(key, value) { try { localStorage.setItem(key, value); } catch { console.warn("localStorage setItem failed"); } } async removeItem(key) { try { localStorage.removeItem(key); } catch { console.warn("localStorage removeItem failed"); } } }; var ReactNativeStorage = class { AsyncStorage; constructor() { try { const AsyncStorage = new Function("return require('@react-native-async-storage/async-storage')")(); this.AsyncStorage = AsyncStorage.default || AsyncStorage; } catch { console.warn("AsyncStorage not available - falling back to memory storage"); this.AsyncStorage = null; } } async getItem(key) { if (!this.AsyncStorage) return null; try { return await this.AsyncStorage.getItem(key); } catch { console.warn("AsyncStorage getItem failed"); return null; } } async setItem(key, value) { if (!this.AsyncStorage) return; try { await this.AsyncStorage.setItem(key, value); } catch { console.warn("AsyncStorage setItem failed"); } } async removeItem(key) { if (!this.AsyncStorage) return; try { await this.AsyncStorage.removeItem(key); } catch { console.warn("AsyncStorage removeItem failed"); } } }; var MemoryStorage = class { storage = /* @__PURE__ */ new Map(); async getItem(key) { return this.storage.get(key) || null; } async setItem(key, value) { this.storage.set(key, value); } async removeItem(key) { this.storage.delete(key); } }; var createAuthStorage = () => { if (typeof window !== "undefined" && window.localStorage) { return new BrowserStorage(); } if (typeof global !== "undefined" && global.navigator?.product === "ReactNative") { return new ReactNativeStorage(); } if (typeof process !== "undefined" && process.versions?.node) { return new MemoryStorage(); } return new MemoryStorage(); }; // src/core/AuthProviderFactory.ts var AuthProviderFactory = class { static instances = /* @__PURE__ */ new Map(); /** * Gets provider based on environment variables (auto-detection) * @param config Optional configuration overrides * @returns Authentication provider instance */ static async getProviderFromEnvironment(config) { const detectedType = this.detectProviderFromEnvironment(); return this.getProvider({ type: detectedType, validateConfig: true, useSingleton: true, ...config }); } /** * Gets provider instance with configuration * @param config Provider configuration * @returns Authentication provider instance */ static async getProvider(config) { const { type, validateConfig: validateConfig2 = false, useSingleton = false, storage } = config; if (useSingleton && this.instances.has(type)) { const instance = this.instances.get(type); if (validateConfig2 && instance.validateConfiguration) { await instance.validateConfiguration(); } return instance; } const provider = this.createProvider(type, storage); if (validateConfig2 && provider.validateConfiguration) { await provider.validateConfiguration(); } if (useSingleton) { this.instances.set(type, provider); } return provider; } /** * Creates a new provider instance without caching * @param type Provider type * @param storage Optional custom storage implementation * @returns Authentication provider instance */ static createProvider(type, storage) { const authStorage = storage || createAuthStorage(); switch (type) { case "supabase": return new SupabaseAuthProvider(authStorage); case "firebase": return new FirebaseAuthProvider(authStorage); default: throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, `Unsupported authentication provider: ${type}. Supported providers: supabase, firebase` ); } } /** * Detects provider type from environment variables * @returns Detected provider type */ static detectProviderFromEnvironment() { const config = getAuthConfig(); const hasSupabaseConfig = !!(config.supabaseUrl && config.supabaseAnonKey); const hasFirebaseConfig = !!(config.firebaseApiKey && config.firebaseAuthDomain && config.firebaseProjectId); const explicitProvider = this.getExplicitProvider(); if (explicitProvider && this.isValidProviderType(explicitProvider)) { return explicitProvider; } if (hasSupabaseConfig && !hasFirebaseConfig) { return "supabase"; } if (hasFirebaseConfig && !hasSupabaseConfig) { return "firebase"; } if (hasSupabaseConfig && hasFirebaseConfig) { console.warn( "Both Supabase and Firebase configurations detected. Set AUTH_PROVIDER environment variable to explicitly choose a provider. Defaulting to Supabase." ); return "supabase"; } throw new AuthError( "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, "No authentication provider configuration found. Please configure either Supabase or Firebase environment variables." ); } /** * Gets explicit provider from environment variables (platform-agnostic) */ static getExplicitProvider() { if (typeof process !== "undefined" && process.env) { return process.env.NEXT_PUBLIC_AUTH_PROVIDER || process.env.AUTH_PROVIDER; } try { const Constants = new Function("return require('expo-constants').default")(); return Constants.expoConfig?.extra?.authProvider; } catch { } try { const Config = new Function("return require('react-native-config').default")(); return Config.AUTH_PROVIDER; } catch { } return void 0; } /** * Validates if a string is a valid provider type * @param type String to validate * @returns True if valid provider type */ static isValidProviderType(type) { return type === "supabase" || type === "firebase"; } /** * Gets list of supported provider types * @returns Array of supported provider types */ static getSupportedProviders() { return ["supabase", "firebase"]; } /** * Checks if a provider type is supported * @param type Provider type to check * @returns True if supported */ static isProviderSupported(type) { return this.isValidProviderType(type); } /** * Clears singleton instances (useful for testing) */ static clearInstances() { this.instances.clear(); } /** * Gets current singleton instances (useful for debugging) * @returns Map of current instances */ static getInstances() { return new Map(this.instances); } }; var useAuthStore = create()( devtools( persist( immer((set, get) => ({ // State user: null, session: null, // Added session to the store loading: true, error: null, authProvider: null, // Initialize authProvider as null // Basic state actions setUser: (user) => set((state) => { state.user = user; state.error = null; }), setSession: (session) => set((state) => { state.session = session; }), // Setter for session setLoading: (loading) => set((state) => { state.loading = loading; }), setError: (error) => set((state) => { state.error = error; }), setAuthProvider: (provider) => set((state) => { state.authProvider = provider; }), // Authentication actions signIn: async (email, password) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { const { user, error } = await provider.signIn(email, password); if (error) throw error; set((state) => { state.user = user; state.loading = false; }); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, signUp: async (email, password) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { const { user, error } = await provider.signUp(email, password); if (error) throw error; set((state) => { state.user = user; state.loading = false; }); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, signOut: async () => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { const { error } = await provider.signOut(); if (error) throw error; set((state) => { state.user = null; state.session = null; state.loading = false; }); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, resetPassword: async (email) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { const { error } = await provider.resetPassword(email); if (error) throw error; set((state) => { state.loading = false; }); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, updateProfile: async (updates) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { const { user, error } = await provider.updateProfile(updates); if (error) throw error; set((state) => { state.user = user; if (state.session && user) { state.session.user = user; } state.loading = false; }); return user; } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, // Extended auth methods (delegated to provider) signInWithOAuth: async (providerName, redirectTo) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { await provider.signInWithOAuth(providerName, redirectTo); set((state) => { state.loading = false; }); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, signInWithMagicLink: async (email, redirectTo) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); set((state) => { state.loading = true; state.error = null; }); try { await provider.signInWithMagicLink(email, redirectTo); set((state) => { state.loading = false; }); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); state.loading = false; }); throw error; } }, getUser: async () => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); try { const user = await provider.getUser(); set((state) => { state.user = user; }); return user; } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); }); throw error; } }, getSession: async () => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); try { return await provider.getSession(); } catch (error) { set((state) => { state.error = error instanceof Error ? error : new Error(String(error)); }); throw error; } }, onAuthStateChange: (callback) => { const provider = get().authProvider; if (!provider) throw new Error("Auth provider not initialized."); return provider.onAuthStateChange(callback); } })), { name: "auth-storage", storage: createJSONStorage(() => localStorage), partialize: (state) => Object.fromEntries( Object.entries(state).filter(([key]) => ["user", "session"].includes(key)) ) } ) ) ); var AuthProvider = ({ children, onAuthChange, autoSignIn = true }) => { const authProviderRef = useRef(null); const unsubscribeRef = useRef(null); const setUser = useAuthStore((state) => state.setUser); const setSession = useAuthStore((state) => state.setSession); const setLoading = useAuthStore((state) => state.setLoading); const setError = useAuthStore((state) => state.setError); const setAuthProvider = useAuthStore((state) => state.setAuthProvider); useEffect(() => { let mounted = true; const initializeAuth = async () => { try { const provider = await AuthProviderFactory.getProviderFromEnvironment({ validateConfig: true, useSingleton: true }); if (!mounted) return; authProviderRef.current = provider; setAuthProvider(provider); const subscription = provider.onAuthStateChange((event, session) => { if (!mounted) return; if (session) { setUser(session.user); setSession(session); setError(null); } else { setUser(null); setSession(null); } if (onAuthChange) { onAuthChange(event); } setLoading(false); }); if (subscription?.unsubscribe) { unsubscribeRef.current = subscription.unsubscribe; } if (autoSignIn) { try { const currentSession = await provider.getSession(); if (mounted) { if (currentSession) { setUser(currentSession.user); setSession(currentSession); } else { setUser(null); setSession(null); } setLoading(false); } } catch (error) { console.error("Failed to get initial session:", error); if (mounted) { setError(error instanceof Error ? error : new Error("Failed to get session")); setLoading(false); } } } else { if (mounted) { setLoading(false); } } } catch (error) { console.error("Failed to initialize auth provider:", error); if (mounted) { setError(error instanceof Error ? error : new Error("Failed to initialize auth")); setLoading(false); } } }; initializeAuth(); return () => { mounted = false; if (unsubscribeRef.current) { unsubscribeRef.current(); unsubscribeRef.current = null; } }; }, []); return /* @__PURE__ */ jsx(Fragment, { children }); }; // src/hooks/useAuth.ts var useAuth = () => { const authState = useAuthStore(); if (!authState.authProvider && !authState.loading) { console.warn( "Auth provider not initialized. Make sure to wrap your app with <AuthProvider> component." ); } return authState; }; // src/actions/index.ts var signUp = async (email, password) => { const store = useAuthStore.getState(); return store.signUp(email, password); }; var signIn = async (email, password) => { const store = useAuthStore.getState(); return store.signIn(email, password); }; var signOut = async () => { const store = useAuthStore.getState(); return store.signOut(); }; var resetPassword = async (email) => { const store = useAuthStore.getState(); return store.resetPassword(email); }; var updateProfile = async (updates) => { const store = useAuthStore.getState(); if (!store.user) { throw new Error("No user logged in"); } return await store.updateProfile(updates); }; var signInWithOAuth = async (provider, redirectTo) => { const store = useAuthStore.getState(); return store.signInWithOAuth(provider, redirectTo); }; var signInWithMagicLink = async (email, redirectTo) => { const store = useAuthStore.getState(); return store.signInWithMagicLink(email, redirectTo); }; var createClient2 = () => { if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { throw new Error("Missing Supabase environment variables"); } return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ); }; (() => { try { if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { return createClient2(); } } catch { console.warn("Supabase client not initialized: environment variables not available"); } return null;