I am using a combination of React Hook, Next.js, and Firebase Auth to create a user coin record in Firestore. This means that as soon as a new user signs up, a new document with a uid
should be automatically created for them in Firestore, along with the coins
and createdAt
fields.
However, when I sign in for the first time, I encounter an error message that says:
hydration-error-info.js:27 Error in createUserCoinRecord: FirebaseError: Missing or insufficient permissions."
However, if I log out and sign in again, the error does not occur anymore.
Upon checking the Firebase database, I noticed that the uid
for the user is not created after the first sign-in but is only created after the second sign-in.
Here's the code for the React Hook:
export function useCoinRecord() {
const { data: session } = useSession();
const userEmailRef = useRef(null);
const [created, setCreated] = useState(false);
async function createUserCoinRecord(uid) {
await setDoc(doc(db, "users", uid), {
coins: 100000,
createdAt: serverTimestamp(),
});
}
useEffect(() => {
async function createCoinRecordIfNeeded() {
if (session) {
if (userEmailRef.current !== session.user.email) {
userEmailRef.current = session.user.email;
try {
await createUserCoinRecord(session.user.email);
setCreated(true);
} catch (error) {
console.error("Error in createUserCoinRecord:", error);
}
}
} else {
userEmailRef.current = null;
}
}
createCoinRecordIfNeeded();
}, [session]);
return { created };
}
And these are my Firestore rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
match /users/{uid}/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
The Sign in button:
import { signIn } from "next-auth/react";
//...
<button onClick={() => signIn("google")}>
Sign In
</button>
Sign in with Next Auth:
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
};
export default NextAuth(authOptions);
Then use useEffect
to sign in with Firebase:
import { useSession } from "next-auth/react";
import { auth, db } from "../firebase/firebase";
//...
const { data: session } = useSession();
const { coinBalance, setCoinBalance } = useCoinBalanceContext();
const [readyToFetchBalance, setReadyToFetchBalance] = useState(false);
useEffect(() => {
if (session) {
signInWithFirebase(session).then((uid) => {
if (uid) {
setReadyToFetchBalance(true);
}
});
}
}, [session]);
const signInWithFirebase = async (session) => {
const response = await fetch("/api/firebase/create-custom-token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ user: session.user }),
});
const customToken = await response.json();
return signInWithCustomToken(auth, customToken.token)
.then((userCredential) => {
return userCredential.user.uid;
})
.catch((error) => {
console.error("Firebase sign-in error:", error);
});
};
const { created } = useCoinRecord();
useEffect(() => {
if (readyToFetchBalance && created && session?.user?.email) {
(async () => {
const balance = await getCoinBalance(session.user.email);
balance && setCoinBalance(balance);
})();
}
}, [session, readyToFetchBalance, created]);
/api/firebase/create-custom-token
import { adminAuth } from "@/firebase/firebaseAdmin";
export default async function handler(req, res) {
if (req.method !== "POST") {
res.status(405).json({ message: "Method not allowed" });
return;
}
const { user } = req.body;
const uid = user.email;
try {
const customToken = await adminAuth.createCustomToken(uid);
res.status(200).json({ token: customToken });
} catch (error) {
console.error("Error creating custom token:", error);
res.status(500).json({ message: "Error creating custom token" });
}
}
firebase.js
import { getApp, getApps, initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: process.env.FIREBASE_APIKEY,
authDomain: process.env.FIREBASE_AUTHDOMAIN,
databaseURL: process.env.FIREBASE_DATABASEURL,
projectId: process.env.PROJECTID,
storageBucket: process.env.FIREBASE_STORAGEBUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGINGSENDERID,
appId: process.env.FIREBASE_APPID,
measurementId: process.env.FIREBASE_MEASUREMENTID,
};
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
export { db, auth };
firebaseAdmin.js
import admin from "firebase-admin";
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_KEY);
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}
const adminDb = admin.firestore();
const adminAuth = admin.auth();
export { adminDb, adminAuth };
My database is structured like this:
/users/[email protected]/coins
where [email protected]
is the uid
.
What could be causing the FirebaseError and how can I fix it please?
I realized that there is a race between two hooks that are running at the same time.
The issue is that useCoinRecord()
tries to create a new document before the user is authenticated. To fix this, I removed useCoinRecord()
and modified the useEffect()
hook to include the necessary logic for creating the user coin record before setting the ReadyToFetchBalance
flag to true.
The updated code looks like this:
useEffect(() => {
if (session) {
signInWithFirebase(session).then(async (uid) => {
if (uid) {
await createUserCoinRecord(uid);
setReadyToFetchBalance(true);
}
});
}
}, [session]);
This should solve the race condition problem and ensure that the document is only created after the user is authenticated.