So I have this issue where every time I add a new user account, it kicks out the current user that is already signed in. I read the firebase api and it said that "If the new account was created, the user is signed in automatically" But they never said anything else about avoiding that.
//ADD EMPLOYEES
addEmployees: function(formData){
firebase.auth().createUserWithEmailAndPassword(formData.email, formData.password).then(function(data){
console.log(data);
});
},
I'm the admin and I'm adding accounts into my site. I would like it if I can add an account without being signed out and signed into the new account. Any way i can avoid this?
Update 20161110 - original answer below
Also, check out this answer for a different approach.
Original answer
This is actually possible.
But not directly, the way to do it is to create a second auth reference and use that to create users:
var config = {apiKey: "apiKey",
authDomain: "projectId.firebaseapp.com",
databaseURL: "https://databaseName.firebaseio.com"};
var secondaryApp = firebase.initializeApp(config, "Secondary");
secondaryApp.auth().createUserWithEmailAndPassword(em, pwd).then(function(firebaseUser) {
console.log("User " + firebaseUser.uid + " created successfully!");
//I don't know if the next statement is necessary
secondaryApp.auth().signOut();
});
If you don't specify which firebase connection you use for an operation it will use the first one by default.
Source for multiple app references.
EDIT
For the actual creation of a new user, it doesn't matter that there is nobody or someone else than the admin, authenticated on the second auth reference because for creating an account all you need is the auth reference itself.
The following hasn't been tested but it is something to think about
The thing you do have to think about is writing data to firebase. Common practice is that users can edit/update their own user info so when you use the second auth reference for writing this should work. But if you have something like roles or permissions for that user make sure you write that with the auth reference that has the right permissions. In this case, the main auth is the admin and the second auth is the newly created user.
Update 20161108 - original answer below
Firebase just released its firebase-admin SDK, which allows server-side code for this and other common administrative use-cases. Read the installation instructions and then dive into the documentation on creating users.
original answer
This is currently not possible. Creating an Email+Password user automatically signs that new user in.
I just created a Firebase Function that triggers when a Firestore document is Created (with rules write-only to admin user). Then use admin.auth().createUser() to create the new user properly.
export const createUser = functions.firestore
.document('newUsers/{userId}')
.onCreate(async (snap, context) => {
const userId = context.params.userId;
const newUser = await admin.auth().createUser({
disabled: false,
displayName: snap.get('displayName'),
email: snap.get('email'),
password: snap.get('password'),
phoneNumber: snap.get('phoneNumber')
});
// You can also store the new user in another collection with extra fields
await admin.firestore().collection('users').doc(newUser.uid).set({
uid: newUser.uid,
email: newUser.email,
name: newUser.displayName,
phoneNumber: newUser.phoneNumber,
otherfield: snap.get('otherfield'),
anotherfield: snap.get('anotherfield')
});
// Delete the temp document
return admin.firestore().collection('newUsers').doc(userId).delete();
});
You can Algo use functions.https.onCall()
exports.createUser= functions.https.onCall((data, context) => {
const uid = context.auth.uid; // Authorize as you want
// ... do the same logic as above
});
calling it.
const createUser = firebase.functions().httpsCallable('createUser');
createUser({userData: data}).then(result => {
// success or error handling
});
Swift 5: Simple Solution
First store the current user in a variable called originalUser
let originalUser = Auth.auth().currentUser
Then, in the completion handler of creating a new user, use the updateCurrentUser method to restore the original user
Auth.auth().updateCurrentUser(originalUser, completion: nil)
Here is a simple solution using web SDKs.
Create a cloud function (https://firebase.google.com/docs/functions)
import admin from 'firebase-admin';
import * as functions from 'firebase-functions';
const createUser = functions.https.onCall((data) => {
return admin.auth().createUser(data)
.catch((error) => {
throw new functions.https.HttpsError('internal', error.message)
});
});
export default createUser;
Call this function from your app
import firebase from 'firebase/app';
const createUser = firebase.functions().httpsCallable('createUser');
createUser({ email, password })
.then(console.log)
.catch(console.error);
Optionally, you can set user document information using the returned uid.
createUser({ email, password })
.then(({ data: user }) => {
return database
.collection('users')
.doc(user.uid)
.set({
firstname,
lastname,
created: new Date(),
});
})
.then(console.log)
.catch(console.error);
I got André's very clever workaround working in Objective-C using the Firebase iOS SDK:
NSString *plistPath = [[NSBundle mainBundle] pathForResource:#"GoogleService-Info" ofType:#"plist"];
FIROptions *secondaryAppOptions = [[FIROptions alloc] initWithContentsOfFile:plistPath];
[FIRApp configureWithName:#"Secondary" options:secondaryAppOptions];
FIRApp *secondaryApp = [FIRApp appNamed:#"Secondary"];
FIRAuth *secondaryAppAuth = [FIRAuth authWithApp:secondaryApp];
[secondaryAppAuth createUserWithEmail:user.email
password:user.password
completion:^(FIRUser * _Nullable user, NSError * _Nullable error) {
[secondaryAppAuth signOut:nil];
}];
Update for Swift 4
I have tried a few different options to create multiple users from a single account, but this is by far the best and easiest solution.
Original answer by Nico
First Configure firebase in your AppDelegate.swift file
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
FirebaseApp.configure(name: "CreatingUsersApp", options: FirebaseApp.app()!.options)
return true
}
Add the following code to action where you are creating the accounts.
if let secondaryApp = FirebaseApp.app(name: "CreatingUsersApp") {
let secondaryAppAuth = Auth.auth(app: secondaryApp)
// Create user in secondary app.
secondaryAppAuth.createUser(withEmail: email, password: password) { (user, error) in
if error != nil {
print(error!)
} else {
//Print created users email.
print(user!.email!)
//Print current logged in users email.
print(Auth.auth().currentUser?.email ?? "default")
try! secondaryAppAuth.signOut()
}
}
}
}
You can use firebase function for add users.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const cors = require('cors')({
origin: true,
});
exports.AddUser = functions.https.onRequest(( req, res ) => {
// Grab the text parameter.
cors( req, res, () => {
let email = req.body.email;
let passwd = req.body.passwd;
let role = req.body.role;
const token = req.get('Authorization').split('Bearer ')[1];
admin.auth().verifyIdToken(token)
.then(
(decoded) => {
// return res.status(200).send( decoded )
return creatUser(decoded);
})
.catch((err) => {
return res.status(401).send(err)
});
function creatUser(user){
admin.auth().createUser({
email: email,
emailVerified: false,
password: passwd,
disabled: false
})
.then((result) => {
console.log('result',result);
return res.status(200).send(result);
}).catch((error) => {
console.log(error.message);
return res.status(400).send(error.message);
})
}
});
});
CreateUser(){
//console.log('Create User')
this.submitted = true;
if (this.myGroup.invalid) {
return;
}
let Email = this.myGroup.value.Email;
let Passwd = this.myGroup.value.Passwd;
let Role = 'myrole';
let TechNum = this.myGroup.value.TechNum;
let user = JSON.parse(localStorage.getItem('user'));
let role = user.role;
let AdminUid = user.uid;
let authToken = user.stsTokenManager.accessToken;
let httpHeaders = new HttpHeaders().set('Authorization', 'Bearer ' + authToken);
let options = { headers: httpHeaders };
let params = { email:Email,passwd:Passwd,role:Role };
this.httpClient.post('https://us-central1-myproject.cloudfunctions.net/AddUser', params, options)
.subscribe( val => {
//console.log('Response from cloud function', val );
let createdUser:any = val;
//console.log(createdUser.uid);
const userRef: AngularFirestoreDocument<any> = this.afs.doc(`users/${createdUser.uid}`);
const userUpdate = {
uid: createdUser.uid,
email: createdUser.email,
displayName: null,
photoURL: null,
emailVerified: createdUser.emailVerified,
role: Role,
TechNum:TechNum,
AccountAccess:this.AccountAccess,
UserStatus:'open',
OwnerUid:AdminUid,
OwnerUidRole:role,
RootAccountAccess:this.RootAccountAccess
}
userRef.set(userUpdate, {
merge: false
});
this.toastr.success('Success, user add','Success');
this.myGroup.reset();
this.submitted = false;
},
err => {
console.log('HTTP Error', err.error)
this.toastr.error(err.error,'Error')
},
() => console.log('HTTP request completed.')
);
}
On the web, this is due to unexpected behavior when you call createUserWithEmailAndPassword out of the registration context; e.g. inviting a new user to your app by creating a new user account.
Seems like, createUserWithEmailAndPassword method triggers a new refresh token and user cookies are updated too. (This side-effect is not documented)
Here is a workaround for Web SDK:
After creating the new user;
firebase.auth().updateCurrentUser (loggedInUser.current)
provided that you initiate loggedInUser with the original user beforehand.
Hey i had similar problem ,trying to create users through admin , as it is not possible to signUp user without signIn ,I created a work around ,adding it below with steps
Instead of signup create a node in firebase realtime db with email as key (firebase do not allow email as key so I have created a function to generate key from email and vice versa, I will attach the functions below)
Save a initial password field while saving user (can even hash it with bcrypt or something, if you prefer though it will be used one time only)
Now Once user try to login check if any node with that email (generate key from email) exist in the db and if so then match the password provided.
If the password matched delete the node and do authSignUpWithEmailandPassword with provided credentials.
User is registered successfully
//Sign In
firebaseDB.child("users").once("value", (snapshot) => {
const users = snapshot.val();
const userKey = emailToKey(data.email);
if (Object.keys(users).find((key) => key === userKey)) {
setError("user already exist");
setTimeout(() => {
setError(false);
}, 2000);
setLoading(false);
} else {
firebaseDB
.child(`users`)
.child(userKey)
.set({ email: data.email, initPassword: data.password })
.then(() => setLoading(false))
.catch(() => {
setLoading(false);
setError("Error in creating user please try again");
setTimeout(() => {
setError(false);
}, 2000);
});
}
});
//Sign Up
signUp = (data, setLoading, setError) => {
auth
.createUserWithEmailAndPassword(data.email, data.password)
.then((res) => {
const userDetails = {
email: res.user.email,
id: res.user.uid,
};
const key = emailToKey(data.email);
app
.database()
.ref(`users/${key}`)
.remove()
.then(() => {
firebaseDB.child("users").child(res.user.uid).set(userDetails);
setLoading(false);
})
.catch(() => {
setLoading(false);
setError("error while registering try again");
setTimeout(() => setError(false), 4000);
});
})
.catch((err) => {
setLoading(false);
setError(err.message);
setTimeout(() => setError(false), 4000);
});
};
//Function to create a valid firebase key from email and vice versa
const emailToKey = (email) => {
//firebase do not allow ".", "#", "$", "[", or "]"
let key = email;
key = key.replace(".", ",0,");
key = key.replace("#", ",1,");
key = key.replace("$", ",2,");
key = key.replace("[", ",3,");
key = key.replace("]", ",4,");
return key;
};
const keyToEmail = (key) => {
let email = key;
email = email.replace(",0,", ".");
email = email.replace(",1,", "#");
email = email.replace(",2,", "$");
email = email.replace(",3,", "[");
email = email.replace(",4,", "]");
return email;
};
If you want to do it in your front end create a second auth reference use it to create other users and sign out and delete that reference. If you do it this way you won't be signed out when creating a new user and you won't get the error that the default firebase app already exists.
const createOtherUser =()=>{
var config = {
//your firebase config
};
let secondaryApp = firebase.initializeApp(config, "secondary");
secondaryApp.auth().createUserWithEmailAndPassword(email, password).then((userCredential) => {
console.log(userCredential.user.uid);
}).then(secondaryApp.auth().signOut()
)
.then(secondaryApp.delete()
)
}
Update 19.05.2022 - using #angular/fire (latest available = v.7.3.0)
If you are not using firebase directly in your app, but use e.g. #angular/fire for auth purposes only, you can use the same approach as suggested earlier as follows with the #angular/fire library:
import { Auth, getAuth, createUserWithEmailAndPassword } from '#angular/fire/auth';
import { deleteApp, initializeApp } from '#angular/fire/app';
import { firebaseConfiguration } from '../config/app.config'; // <-- Your project's configuration here.
const tempApp = initializeApp(firebaseConfiguration, "tempApp");
const tempAppAuth = getAuth(tempApp);
await createUserWithEmailAndPassword(tempAppAuth, email, password)
.then(async (newUser) => {
resolve( () ==> {
// Do something, e.g. add user info to database
});
})
.catch(error => reject(error))
.finally( () => {
tempAppAuth.signOut()
.then( () => deleteApp(tempApp));
});
The Swift version:
FIRApp.configure()
// Creating a second app to create user without logging in
FIRApp.configure(withName: "CreatingUsersApp", options: FIRApp.defaultApp()!.options)
if let secondaryApp = FIRApp(named: "CreatingUsersApp") {
let secondaryAppAuth = FIRAuth(app: secondaryApp)
secondaryAppAuth?.createUser(...)
}
Here is a Swift 3 adaptaion of Jcabrera's answer :
let bundle = Bundle.main
let path = bundle.path(forResource: "GoogleService-Info", ofType: "plist")!
let options = FIROptions.init(contentsOfFile: path)
FIRApp.configure(withName: "Secondary", options: options!)
let secondary_app = FIRApp.init(named: "Secondary")
let second_auth = FIRAuth(app : secondary_app!)
second_auth?.createUser(withEmail: self.username.text!, password: self.password.text!)
{
(user,error) in
print(user!.email!)
print(FIRAuth.auth()?.currentUser?.email ?? "default")
}
If you are using Polymer and Firebase (polymerfire) see this answer: https://stackoverflow.com/a/46698801/1821603
Essentially you create a secondary <firebase-app> to handle the new user registration without affecting the current user.
Android solution (Kotlin):
1.You need FirebaseOptions BUILDER(!) for setting api key, db url, etc., and don't forget to call build() at the end
2.Make a secondary auth variable by calling FirebaseApp.initializeApp()
3.Get instance of FirebaseAuth by passing your newly created secondary auth, and do whatever you want (e.g. createUser)
// 1. you can find these in your project settings under general tab
val firebaseOptionsBuilder = FirebaseOptions.Builder()
firebaseOptionsBuilder.setApiKey("YOUR_API_KEY")
firebaseOptionsBuilder.setDatabaseUrl("YOUR_DATABASE_URL")
firebaseOptionsBuilder.setProjectId("YOUR_PROJECT_ID")
firebaseOptionsBuilder.setApplicationId("YOUR_APPLICATION_ID") //not sure if this one is needed
val firebaseOptions = firebaseOptionsBuilder.build()
// indeterminate progress dialog *ANKO*
val progressDialog = indeterminateProgressDialog(resources.getString(R.string.progressDialog_message_registering))
progressDialog.show()
// 2. second auth created by passing the context, firebase options and a string for secondary db name
val newAuth = FirebaseApp.initializeApp(this#ListActivity, firebaseOptions, Constants.secondary_db_auth)
// 3. calling the create method on our newly created auth, passed in getInstance
FirebaseAuth.getInstance(newAuth).createUserWithEmailAndPassword(email!!, password!!)
.addOnCompleteListener { it ->
if (it.isSuccessful) {
// 'it' is a Task<AuthResult>, so we can get our newly created user from result
val newUser = it.result.user
// store wanted values on your user model, e.g. email, name, phonenumber, etc.
val user = User()
user.email = email
user.name = name
user.created = Date().time
user.active = true
user.phone = phone
// set user model on /db_root/users/uid_of_created_user/, or wherever you want depending on your structure
FirebaseDatabase.getInstance().reference.child(Constants.db_users).child(newUser.uid).setValue(user)
// send newly created user email verification link
newUser.sendEmailVerification()
progressDialog.dismiss()
// sign him out
FirebaseAuth.getInstance(newAuth).signOut()
// DELETE SECONDARY AUTH! thanks, Jimmy :D
newAuth.delete()
} else {
progressDialog.dismiss()
try {
throw it.exception!!
// catch exception for already existing user (e-mail)
} catch (e: FirebaseAuthUserCollisionException) {
alert(resources.getString(R.string.exception_FirebaseAuthUserCollision), resources.getString(R.string.alertDialog_title_error)) {
okButton {
isCancelable = false
}
}.show()
}
}
}
For Android, i suggest a simpler way to do it, without having to provide api key, application id...etc by hand by just using the FirebaseOptions of the default instance.
val firebaseDefaultApp = Firebase.auth.app
val signUpAppName = firebaseDefaultApp.name + "_signUp"
val signUpApp = try {
FirebaseApp.initializeApp(
context,
firebaseDefaultApp.options,
signUpAppName
)
} catch (e: IllegalStateException) {
// IllegalStateException is throw if an app with the same name has already been initialized.
FirebaseApp.getInstance(signUpAppName)
}
// Here is the instance you can use to sign up without triggering auth state on the default Firebase.auth
val signUpFirebaseAuth = Firebase.auth(signUpApp)
How to use ?
signUpFirebaseAuth
.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
// Optional, you can send verification email here if you need
// As soon as the sign up with sign in is over, we can sign out the current user
firebaseAuthSignUp.signOut()
}
.addOnFailureListener {
// Log
}
My solution to this question is to store the User Name/Email and password in a static class and then add a new user log out the new user and immediately log in as the admin user(id pass you saved). Works like a charm for me :D
This is a version for Kotlin:
fun createUser(mail: String, password: String) {
val opts = FirebaseOptions.fromResource(requireContext())
if (opts == null) return
val app = Firebase.initialize(requireContext(), opts, "Secondary")
FirebaseAuth.getInstance(app)
.createUserWithEmailAndPassword(mail, password)
.addOnSuccessListener {
app.delete()
doWhateverWithAccount(it)
}.addOnFailureListener {
app.delete()
showException(it)
}
}
It uses the configuration from your default Firebase application instance, just under a different name.
It also deletes the newly created instance afterwards, so you can call this multiple times without any exception about already existing Secondary application.
I am using a JS script to implement the Stripe payment provider on a website. The code mainly comes from this tutorial from Stripe. The full code can be found here.
Part of the script involves promises, which I am very unfamiliar with. Essentially, I need to go through a number of .then blocks to handle a customer's checkout and subscribe them to the service. When I run the code, I get the following error:
TypeError: Cannot read property 'subscription' of undefined
Below is the function causing the error, with the problematic .then block noted:
var createSubscription = async function ({ customerIdInput, paymentMethodIdInput, priceIdInput }) {
var handlePaymentThatRequiresCustomerAction = function ({
subscription,
invoice,
priceId,
paymentMethodId,
}) {
{
if (subscription && subscription.status === 'active') {
// Subscription is active, no customer actions required.
return { subscription, priceId, paymentMethodId };
}
// If it's a first payment attempt, the payment intent is on the subscription latest invoice.
// If it's a retry, the payment intent will be on the invoice itself.
let paymentIntent = invoice ? invoice.payment_intent : subscription.latest_invoice.payment_intent;
if (
paymentIntent.status === 'requires_action'
) {
return stripe
.confirmCardPayment(paymentIntent.client_secret, {
payment_method: paymentMethodId,
})
.then((result) => {
if (result.error) {
// Start code flow to handle updating the payment details.
// Display error message in your UI.
// The card was declined (i.e. insufficient funds, card has expired, etc).
throw result;
} else {
if (result.paymentIntent.status === 'succeeded') {
// Show a success message to your customer.
return {
priceId: priceId,
subscription: subscription,
invoice: invoice,
paymentMethodId: paymentMethodId,
};
}
}
})
.catch((error) => {
displayError(error);
});
} else {
// No customer action needed.
return { subscription, priceId, paymentMethodId };
}
} };
var handleRequiresPaymentMethod = function () {
};
var onSubscriptionComplete = function (result) {
if (result.subscription.status === 'active') {
orderComplete();
}
};
var subscriptionParams = {
"paymentMethodId": `${paymentMethodIdInput}`,
"customerId": `${customerIdInput}`,
"priceId": `${priceIdInput}`
};
await fetch('/create-subscription', {
method: 'post',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(subscriptionParams),
})
.then((response) => {
return response.json()
})
// If the card is declined, display an error to the user.
.then((result) => {
if (result.error) {
showError(result);
// The card had an error when trying to attach it to a customer.
throw result;
}
var output = Promise.resolve(result);
return output;
})
// Normalize the result to contain the object returned by Stripe.
// Add the additional details we need.
.then((output) => {
return new Promise(function (resolve) {
resolve({
paymentMethodId: paymentMethodIdInput,
priceId: priceIdInput,
subscription: output,
});
});
})
// Some payment methods require a customer to be on session
// to complete the payment process. Check the status of the
// payment intent to handle these actions.
//THIS IS THE THEN BLOCK CAUSING THE ERROR
.then((value) => {
handlePaymentThatRequiresCustomerAction({
subscription: value.subscription,
invoice: value.subscription.latest_invoice,
priceId: value.priceId,
paymentMethodId: value.paymentMethodId,
});
})
// If attaching this card to a Customer object succeeds,
// but attempts to charge the customer fail, you
// get a requires_payment_method error.
.then(() => handleRequiresPaymentMethod())
// No more actions required. Provision your service for the user.
.then(() => {
onSubscriptionComplete();
})
.catch((error) => {
// An error has happened. Display the failure to the user here.
// We utilize the HTML element we created.
showError(error);
});
}
I'm 90% certain that the .then block preceding the error (below) is not fully executing before the problematic block tries to reference value.subscription, and that that is causing the error. However, I'm not sure how to fix that. I've tried using a Jasmine test to isolate the problem, but I've been unsuccessful in that avenue.
.then((output) => {
return new Promise(function (resolve) {
resolve({
paymentMethodId: paymentMethodIdInput,
priceId: priceIdInput,
subscription: output,
});
});
})
//THIS IS THE THEN BLOCK CAUSING THE ERROR
.then((value) => {
handlePaymentThatRequiresCustomerAction({
subscription: value.subscription,
invoice: value.subscription.latest_invoice,
priceId: value.priceId,
paymentMethodId: value.paymentMethodId,
});
})
What can I do to ensure that everything executes in the proper order and avoid this error? Thanks for your help.
UPDATE: as Zac stated in the comments, the issue was the OnSubscriptionComplete() function, which I neglected to pass a value to.
I am having a slightly odd issue, and due to the lack of errors, I am not exactly sure what I am doing wrong. What I am trying to do is on an onCreate event, make an API call, and then update a field on the database if the field is not set to null. Based on my console logs for cloud functions, I can see the API call getting a ok, and everything is working properly, but after about 2-5 minutes, it will update. A few times, it didnt update after 15 mins. What is causing such a slow update?
I have eliminated the gaxios call as the bottleneck simply from the functions logs, and local testing.
Some context: I am on the firebase blaze plan to allow for egress and my dataset isnt really big. I am using gaxios because it is already part of firebase-funcstions npm install.
The code is:
const functions = require('firebase-functions');
const { request } = require('gaxios');
const { parse } = require('url');
exports.getGithubReadme = functions.firestore.document('readmes/{name}').onCreate((snapshot, context) => {
const toolName = context.params.name;
console.log(toolName);
const { name, description, site } = snapshot.data();
console.log(name, description, site);
const parsedUrl = parse(site);
console.log(parsedUrl);
if (description) return;
if (parsedUrl.hostname === 'github.com') {
let githubUrl = `https://api.github.com/repos${parsedUrl.path}/readme`;
request({
method : 'GET',
url : githubUrl
})
.then((res) => {
let { content } = res.data;
return snapshot.ref.update({ description: content });
})
.catch((error) => {
console.log(error);
return null;
});
}
return null;
});
When you execute an asynchronous operation (i.e. request() in your case) in a background triggered Cloud Function, you must return a promise, in such a way the Cloud Function waits that this promise resolves in order to terminate.
This is very well explained in the official Firebase video series here (Learning Cloud Functions for Firebase (video series)). In particular watch the three videos titled "Learn JavaScript Promises" (Parts 2 & 3 especially focus on background triggered Cloud Functions, but it really worth watching Part 1 before).
So you should adapt your code as follows, returning the promise returned by request():
const functions = require('firebase-functions');
const { request } = require('gaxios');
const { parse } = require('url');
exports.getGithubReadme = functions.firestore.document('readmes/{name}').onCreate((snapshot, context) => {
const toolName = context.params.name;
console.log(toolName);
const { name, description, site } = snapshot.data();
console.log(name, description, site);
const parsedUrl = parse(site);
console.log(parsedUrl);
if (description) return null;
if (parsedUrl.hostname === 'github.com') {
let githubUrl = `https://api.github.com/repos${parsedUrl.path}/readme`;
return request({
method: 'GET',
url: githubUrl
})
.then((res) => {
let { content } = res.data;
return snapshot.ref.update({ description: content });
})
.catch((error) => {
console.log(error);
return null;
});
} else {
return null;
}
});
guys!
i am developing an app similar to https://airtasker.com where users outsource tasks.
the taskers would bid to the tasks, and wait for the user to approve their bids.
these are the involved collections:
tasks
transactions
bids
basically, this function should:
check if a transaction exists with the given taskId.
a transaction is added if the user starts to approve bids. i allow multiple taskers to complete the task.
if a transaction doesn't exist, it should
add a new one, mark the status ongoing if it reaches the required manpower (otherwise pending), and update the bids collection to mark the bid accepted.
if a transaction exists, it should
check if the current approved list from the transactions collection is equal to the manpower
if it hasn't reached the quota manpower yet, push a new tasker and access the bids collection to mark the bid accepted.
if after the last condition, the approved list already reached the quota manpower, mark the task close, and change the status of the transaction as ongoing
but i keep getting this error:
Uncaught (in promise) Error: Every document read in a transaction must also be written.
at Transaction.commit (transaction.js:128)
at eval (sync_engine.js:244)
here's my code:
const acceptOffer = async (taskerId, taskId, bidId, offer) => {
let bulk
try {
const taskRef = db.collection('tasks').doc(taskId)
const transRef = db.collection('transactions').doc(taskId)
const bidRef = db.collection('bids').doc(bidId)
const fees = solveFees(offer)
bulk = await db
.runTransaction(async t => {
const transdoc = await t.get(transRef)
const taskdoc = await t.get(taskRef)
const manpower = await taskdoc.get('manpower')
let status = 'pending'
if (manpower === 1) {
status = 'ongoing'
}
if (!transdoc.exists) {
t.set(transRef, {
taskId,
status, // pending, ongoing, completed
approved: [
{ taskerId, ...fees }
]
})
t.update(bidRef, {
accepted: true
})
} else {
const approved = await transdoc.get('approved')
if (manpower < approved.length) {
approved.push({ taskerId, ...fees })
t.update(transRef, { approved })
t.update(bidRef, { accepted: true })
if (manpower === approved.length) {
t.update(taskRef, { open: false })
t.update(transRef, { status: 'ongoing' })
}
}
}
})
} catch (e) {
bulk = e
console.log('nag error', e)
throw e
}
if (bulk.success) {
swal('Offer accepted!', '', 'success')
} else {
swal('Oh, no!',
'This task might already be approved',
'error'
)
}
}
i have been stuck here since i don't understand where the transaction failed. any help is very much appreciated.
thank you!
to those who are having the same problem, here is my (hackish) solution:
for every condition,
add a document write (could be a set() update() or delete()) that corresponds to each of the document reads which in my code: the use of get()s.
and return a Promise
here's the updated code:
// a transaction is added if the user starts to approve offers
// this function allows multiple taskers
const acceptOffer = async (taskerId, taskId, bidId, offer) => {
let bulk
try {
const taskRef = db.collection('tasks').doc(taskId)
const transRef = db.collection('transactions').doc(taskId)
const bidRef = db.collection('bids').doc(bidId)
const fees = solveFees(offer)
bulk = await db
.runTransaction(async t => {
const transdoc = await t.get(transRef)
const taskdoc = await t.get(taskRef)
const manpower = await taskdoc.get('manpower')
// check if a transaction exists with the given taskId
// if it doesn't, then the task doesn't have
// any approved bidders yet
if (!transdoc.exists) {
// check if there is only one manpower required for the task
// mark the status of the transaction 'ongoing' if so
const status = manpower === 1
? 'ongoing' : 'pending'
// add a transaction with the approved tasker
t.set(transRef, {
taskId,
status, // pending, ongoing, completed
approved: [
{ taskerId, ...fees }
]
})
// mark the bid 'accepted'
t.update(bidRef, {
accepted: true
})
// hackish (to prevent firestore transaction errors)
t.update(taskRef, {})
return Promise.resolve(true)
} else { // if a transaction exists with the given taskId
const approved = await transdoc.get('approved')
// check if the current approved list from
// the transactions collection hasn't
// reached the manpower quota yet
if (approved.length < manpower) {
// push new approved bid of the tasker
approved.push({ taskerId, ...fees })
t.update(transRef, { approved })
t.update(bidRef, { accepted: true }) // mark the bid 'accepted'
t.update(taskRef, {}) // hackish
// if, after pushing a new transaction,
// the approved list reached the manpower quota
if (approved.length === manpower) {
t.update(taskRef, { open: false }) // mark the task 'close'
t.update(transRef, { status: 'ongoing' }) // mark the transaction 'ongoing'
t.update(bidRef, {}) // hackish
}
return Promise.resolve(true)
}
return Promise.reject(new Error('Task closed!'))
}
})
} catch (e) {
swal('Oh, no!',
'This task might already be closed',
'error'
)
throw e
}
if (bulk) {
swal('Offer accepted!', '', 'success')
}
}
I ran into the same issue. As long as google will not be able to sent validation errors with better errors than just that the client was not allowed to write the data (security rules). I prefer to handle it on client site. So I use transactions for example to validate that a referenced doc is still available when I write data. (for example I have write an order document that references to a customer and want be sure that the customer still exists.) So I have to read it but actually there is no need to write it.
I came up with something close to nrions solution but tried to have a more general approach for it so I wrote a wrapper for runTransaction. Of cause it is not the cleanest way to do it but maybe it is useful for others.
// Transaction neads to write all docs read be transaction.get().
// To work around this we we call an update with {} for each document requested by transaction.get() before writing any data
export function runTransaction(updateFunction) {
return db.runTransaction(transaction => {
const docRefsRequested = [];
let didSetRequestedDocs = false;
function setEachRequestedDoc() {
if (didSetRequestedDocs) {
return;
}
didSetRequestedDocs = true;
docRefsRequested.forEach(({ exists, ref }) => {
if (exists) {
transaction.update(ref, {});
} else {
transaction.delete(ref);
}
});
}
const transactionWrapper = {
get: function(documentRef) {
return transaction.get(ref).then(snapshot => {
const { exists } = snapshot;
docRefsRequested.push({ ref, exists });
return Promise.resolve(snapshot);
});
},
set: function(documentRef, data) {
setEachRequestedDoc();
return transaction.set(documentRef, data);
},
update: function(documentRef, data) {
setEachRequestedDoc();
return transaction.update(documentRef, data);
},
delete: function(documentRef) {
setEachRequestedDoc();
return transaction.delete(documentRef);
},
};
return updateFunction(transactionWrapper).then(resolveValue => {
setEachRequestedDoc();
return Promise.resolve(resolveValue);
});
});
}