BenjaminK
BenjaminK

Reputation: 813

How to await the creation of a document created in cloud functions with onSnapshot

I want to securely create a user document onCreate that is in sync with the auth.user database in Firebase v9. I think it wouldn't be secure to let a registered user create a user document. So I wrote a cloud function which triggers on functions.auth.user().onCreate() and creates a user document.

Problem:

I have the problem keeping them in sync as the onSnapshotmethod which should await for the user document to exists already returns a promise if the user document does not yet exists. Sometimes it works and sometimes not. So I don't know when I can update the by the cloud function created user document.

Question:

Why does the onSnapshot sometimes work and sometimes not. How can I fix it?

Here is a link to a helpful Article which seem to doesn't work in v9. Link

I tried and searched everywhere. I can't believe this is not a standard feature and still a requested topic. This seems so basic.

Error

error FirebaseError: No document to update: as const user = await createAccount(displayName, email, password); returns even if user is not yet in doc.data()

Sign Up function

interface SignUpFormValues {
    email: string;
    password: string;
    confirm: string;
    firstName: string;
    lastName: string;
  }

const createAccount = async (
    displayName: string,
    email: string,
    password: string
  ) => {
    // Create auth user
    const userCredential = await createUserWithEmailAndPassword(
      auth,
      email,
      password
    );
    // -> Signed in

    // Update Profile
    const user = userCredential.user;
    const uid = user.uid;
    await updateProfile(user, {
      displayName: displayName,
    });

    // IMPORTANT: Force refresh regardless of token expiration
    // auth.currentUser.getIdToken(true); // -> will stop the onSnapshot function from resolving properly

    // Build a reference to their per-user document
    const userDocRef = doc(db, "users", uid);

    return new Promise((resolve, reject) => {
      const unsubscribe = onSnapshot(userDocRef, {
        next: (doc) => {
          unsubscribe();
          console.log("doc", doc); // -> returning undefined
          console.log("doc.data()", doc.data()); // -> returning undefined
          resolve(user); // -> returning undefined
        },
        error: (error) => {
          unsubscribe();
          console.log("error", error);
          reject(error);
        },
      });
    });
  };

const handleSignUp = async (values: SignUpFormValues) => {
    const { firstName, lastName, email, password } = values;
    const displayName = `${firstName} ${lastName}`;

    try {
      setError("");
      setLoading(true);

      // Create user account
      const user = await createAccount(displayName, email, password);
      console.log("createAccount -> return:", user); // -> problem here sometimes return undefined

      // Update user
      const newUserData = {
        displayName: displayName,
        firstName,
        lastName,
      };
      // Build a reference to their per-user document
      const userDocRef = doc(db, "users", user.uid);
      await updateDoc(userDocRef, newUserData);

      // Send Email verification
      await authSendEmailVerification(user);

      // Logout
      await logout();

      navigate("/sign-up/email-verification", { state: values });
    } catch (error: any) {
      const errorCode = error.code;
      const errorMessage = error.message;
      console.log("error", error);
      console.log("error", errorCode);
      if (errorCode === "auth/email-already-in-use") {
        const errorMessage =
          "Failed to create an account. E-Mail address is already registered.";
        setError(errorMessage);
        console.log("error", errorMessage);
      } else {
        setError("Failed to create account.");
      }
    }
    setLoading(false);
  };

Cloud function which triggers the user onCreate

// On auth user create
export const authUserWriteListener = functions.auth
  .user()
  .onCreate(async (user, context) => {
    console.log("user:", user);
    const userRef = db.doc(`users/${user.uid}`);
    await userRef.set({
      email: user.email,
      createdAt: context.timestamp,
      firstTimeLogin: true,
    });

    return db.doc("stats/users").update({
      totalDocsCount: FieldValue.increment(1),
    });
  });

Upvotes: 1

Views: 146

Answers (1)

Greg Fenton
Greg Fenton

Reputation: 2808

The issue is that the Cloud Function code runs asynchronously. There is no guarantee that it will run quickly enough to have the document created in Firestore between the end of createAccount() and your call to updateDoc(). In fact, if your system has been idle for a while it could be a minute (or more!) for the Cloud Function to execute (do a search for "cold start firebase cloud functions").

One option, depending on your design, might be to not take in first name and last name during sign up? But instead take the user to a "profile page" once they are logged in where they could modify aspects of their profile (by that time the user profile document hopefully is created). On that page, if the get() returns no document, you could put up a notification to the user that the system "is still processing their registration" or something like that.

Upvotes: 2

Related Questions