import React, { useState, useEffect, PropsWithChildren } from "react"; import * as Sentry from "@sentry/react-native"; import { FirebaseUser, firebaseService } from "@/services"; import { CreateEmailUserArgs, useAuth, useCreateNewNostrAccount, useCreateNewUser, useCreateNewVerifiedUser, usePrivateUserData, UserContext, useToast, } from "@/hooks"; import { getKeysFromNostrSecret, getSecretFromKeychain, saveSecretToKeychain, useAddPubkeyToUser, } from "@/utils"; interface AuthState { user: FirebaseUser | null; initializingAuth: boolean; } export const UserContextProvider: React.FC = ({ children, }) => { const { mutateAsync: createUser } = useCreateNewUser(); const { mutateAsync: createVerifiedUser } = useCreateNewVerifiedUser(); const toast = useToast(); const { pubkey, login } = useAuth(); const createNewNostrAccount = useCreateNewNostrAccount(); const { mutateAsync: addPubkeyToAccount } = useAddPubkeyToUser({}); const [authState, setAuthState] = useState({ user: null, initializingAuth: true, }); const { user, initializingAuth } = authState; const { refetch: _refetchUser, data: catalogUser } = usePrivateUserData( Boolean(user), ); const refetchUser = async () => { if (!user) return; const { data } = await _refetchUser(); return data; }; // Nostr account management const createNostrAccountForUser = async (username?: string) => { const { nsec, pubkey } = await createNewNostrAccount( username ? { name: username } : {}, ); if (!pubkey || !nsec) { toast.show("Failed to create nostr account"); throw new Error("Failed to create nostr account"); } return { nsec, pubkey }; }; const handleExistingNostrAccount = async () => { const secret = await getSecretFromKeychain(); if (!secret?.password) return null; await login(secret.password); return getKeysFromNostrSecret(secret.password); }; // User creation and association const associatePubkeyWithUser = async (keys: any, userEmail: string) => { if (!keys?.pubkey || !catalogUser) return false; const pubkeyExists = catalogUser.nostrProfileData .map((data) => data.publicHex) .includes(keys.pubkey); if (pubkeyExists) { await addPubkeyToAccount(); } return pubkeyExists; }; const handleNewUser = async (email: string, username?: string) => { if (!pubkey) { const { nsec, pubkey: newPubkey } = await createNostrAccountForUser(username); saveSecretToKeychain(email, nsec); await login(nsec); return newPubkey; } return pubkey; }; // Auth state management useEffect(() => { const handleAuthStateChange = async (user: FirebaseUser) => { setAuthState({ user, initializingAuth: false }); if (!user) return; const { data: incomingCatalogUser } = await _refetchUser(); if (!incomingCatalogUser?.id) { // Wait for DB record creation and retry await new Promise((resolve) => setTimeout(resolve, 3000)); await _refetchUser(); } }; const unsubscribe = firebaseService.onAuthStateChange( handleAuthStateChange, ); return unsubscribe; }, []); const signInWithGoogle = async () => { try { const user = await firebaseService.signInWithGoogle(); if ("error" in user) throw user.error; const { data: catalogUserData } = await _refetchUser(); if (user.additionalUserInfo?.isNewUser) { const username = user.user.displayName ?? ""; const userPubkey = await handleNewUser(user.user.email ?? "", username); await createUser({ pubkey: userPubkey, username }); } else if (!pubkey) { const keys = await handleExistingNostrAccount(); if (keys) { await associatePubkeyWithUser(keys, user.user.email ?? ""); } } return { ...user, isRegionVerified: catalogUserData?.isRegionVerified, isEmailVerified: user.user.emailVerified, }; } catch (error) { Sentry.captureException(error, { extra: { method: "firebase.signInWithGoogle", }, }); console.error("Google sign-in error:", error); return { error: typeof error === "string" ? error : "Failed to sign in with Google", }; } }; const signInWithApple = async () => { try { const user = await firebaseService.signInWithApple(); if ("error" in user) throw user.error; const { data: catalogUserData } = await _refetchUser(); if (user.additionalUserInfo?.isNewUser) { const username = user.user.displayName ?? ""; const userPubkey = await handleNewUser(user.user.email ?? "", username); await createUser({ pubkey: userPubkey, username }); } else if (!pubkey) { const keys = await handleExistingNostrAccount(); if (keys) { await associatePubkeyWithUser(keys, user.user.email ?? ""); } } return { ...user, isRegionVerified: catalogUserData?.isRegionVerified, isEmailVerified: user.user.emailVerified, }; } catch (error) { Sentry.captureException(error, { extra: { method: "firebase.signInWithApple", }, }); console.error("Apple sign-in error:", error); return { error: typeof error === "string" ? error : "Failed to sign in with Apple", }; } }; const createUserWithEmail = async ({ email, password, username, firstName, lastName, pubkey, isRegionVerified, }: CreateEmailUserArgs) => { try { const user = await firebaseService.createUserWithEmailFirebase( email, password, ); if ("error" in user) throw user.error; await user.user.sendEmailVerification(); const newUser = isRegionVerified ? await createVerifiedUser({ username, firstName, lastName, pubkey, }) : await createUser({ username, pubkey }); if (!newUser) { toast.show("Failed to create user in database"); throw new Error("Failed to create user in database"); } return user; } catch (error) { console.error("Email user creation error:", error); return { error: typeof error === "string" ? error : "Failed to create user", }; } }; const signInWithEmail = async (email: string, password: string) => { try { const user = await firebaseService.signInWithEmail(email, password); if ("error" in user) throw user.error; const { data: catalogUserData } = await _refetchUser(); if (!catalogUserData) { toast.show("Catalog user not found"); throw new Error("Catalog user not found"); } if (!pubkey) { const keys = await handleExistingNostrAccount(); if (keys) { await associatePubkeyWithUser(keys, email); } } else { await associatePubkeyWithUser({ pubkey }, email); } return { ...user, isRegionVerified: catalogUserData?.isRegionVerified, isEmailVerified: user.user.emailVerified, }; } catch (error) { Sentry.captureException(error, { extra: { method: "firebase.signInWithEmail", }, }); console.error("Email sign-in error:", error); return { error: typeof error === "string" ? error : "Failed to sign in", }; } }; const contextValue = { initializingAuth, user, catalogUser: user ? catalogUser : undefined, refetchUser, nostrMetadata: catalogUser?.nostrProfileData.find( (n) => n.publicHex === pubkey, ), ...firebaseService, signInWithApple, signInWithGoogle, signInWithEmail, createUserWithEmail, }; return ( {children} ); };