[RESOLVED] The issue is when I call the function subscribe() after le .then(() => when the account is created in firebase. I think the function is called more than one time.
I use Firebase and stripe to manage subscription payments (through the stripe subscription firebase extension) and have an issue when the user signs up and is added to stripe.
When the users signs up, an account is created in my Firebase app. Then he is redirected to a Stripe checkout session. In between he is added to the customers section of my stripe account (I don't manage that, it is done in the backend through the extension I believe).
Issue -> the customer is created twice in Stripe.
One of the duplicated customer in Stripe has the logs : "POST /v1/customers" and "POST /v1/checkout/session".
The other has only the log "POST /v1/customers".
They both have the same firebaseUID in the metadata section in Stripe. And only one user is created in Firebase.
This is my signUp function which is called through onclick() on a button :
if (agreeToTerms == true && name != '' && surname != '' && companyName != '' && companyActivity != '' ) {
auth.createUserWithEmailAndPassword(email, password).then(cred => {
return db.collection('collection-users').doc(cred.user.uid).set({
name: name,
surname: surname,
email: email,
companyName: companyName,
companyActivity: companyActivity
})
}).then(() => {
// User signed up, now redirected to stripe checkout session to subscribe
subscribe();
container_signup.querySelector(".error").innerHTML = '';
}).catch(err => {
container_signup.querySelector(".error").innerHTML = err.message;
});
} else {
container_signup.querySelector(".error").innerHTML = "Please add all the required informations."
}
And this is my subscribe function which is used to redirect the user to the stripe checkout session (called when the account has been created) :
async function subscribe() {
const docRef = await db
.collection('collection-users')
.doc(firebase.auth().currentUser.uid)
.collection('checkout_sessions')
.add({
price: 'price_id',
success_url: 'https://mywebsite.com',
cancel_url: 'https://mywebsite.com',
});
// Wait for the CheckoutSession to get attached by the extension
docRef.onSnapshot((snap) => {
const { error, url } = snap.data();
if (error) {
// Show an error to your customer and
// inspect your Cloud Function logs in the Firebase console.
alert(`An error occured: ${error.message}`);
}
if (url) {
// We have a Stripe Checkout URL, let's redirect.
window.location.assign(url);
}
});
}
What am I doing wrong?
Thank you very much for your help.
[EDIT] The firebase logs down below. What we can see is that the function .createCustomer creates the first customer while .createCheckoutSession creates not only the checkout session but also another customer (the duplicated one).
7:48:29.200 pm
ext-firestore-stripe-subscriptions-createCustomer
Function execution started
7:48:29.202 pm
info
gmp_mods ext-firestore-stripe-subscriptions-createCustomer
⚙️ Creating customer object for [t5LdbvNH5hSOFe7ECuzpHfWke1x2].
7:48:29.215 pm
outlined_flag
gmp_mods ext-firestore-stripe-subscriptions-createCheckoutSession
Function execution started
7:48:29.218 pm
info
gmp_mods ext-firestore-stripe-subscriptions-createCheckoutSession
⚙️ Creating checkout session for doc [UkCpB5Yqw8c9RR76wyp6].
7:48:29.464 pm
info
gmp_mods ext-firestore-stripe-subscriptions-createCheckoutSession
⚙️ Creating customer object for [t5LdbvNH5hSOFe7ECuzpHfWke1x2].
7:48:29.636 pm
info
gmp_mods ext-firestore-stripe-subscriptions-createCustomer
✅Created a new customer: https://dashboard.stripe.com/test/customers/cus_KMlAFc3l3oAgnd.
7:48:29.637 pm
outlined_flag
gmp_mods ext-firestore-stripe-subscriptions-createCustomer
Function execution took 437 ms, finished with status: 'ok'
7:48:29.925 pm
info
gmp_mods ext-firestore-stripe-subscriptions-createCheckoutSession
✅Created a new customer: https://dashboard.stripe.com/test/customers/cus_KMlAUuGRyge978.
7:48:30.679 pm
info
gmp_mods ext-firestore-stripe-subscriptions-createCheckoutSession
✅Checkout session created for doc [UkCpB5Yqw8c9RR76wyp6].
7:48:30.679 pm
outlined_flag
gmp_mods ext-firestore-stripe-subscriptions-createCheckoutSession
Function execution took 1464 ms, finished with status: 'ok'
Related
Is there any way to wait for a Cloud Function, that was triggered by a Firestore document write, to finish?
Context:
My app has groups. Owners can invite other users to a group via an invite code. Users can write themselves as member of a group if they have the right invite code. They do this by writing the groups/{groupId}/members/{userId} document that contains their profile info.
To make reading more efficient, this info is copied to array members in the groups/{groupId} document by a Cloud Function.
The Cloud Function that does that is triggered by the document write. It is usually finished after a couple of seconds, but there's no predictable execution time and it might take a bit longer if it is a cold start.
After the user has joined the group, I forward them to the groups view in my app which reads the group document. In order for the view to render correctly, the membership info needs to be available. So I would like to forward AFTER the Cloud Function has finished.
I found no way to track the execution of a Cloud Function that was triggered by a Firestore document write.
A fellow developer recommended to just poll the groups/{groupId} document until the info is written and then proceed but this doesn't seem like a clean solution to me.
Any ideas how this could be done better?
Is it possible to get a promise that resolves after the Cloud Function has finished? Is there a way to combine a Firestore document write and a Cloud Function execution into one transaction?
Thanks for the hints, I came up with the following ways to deal with the problem. The approach depends on if/when the user is allowed to read a document:
A) User is member and leaves the group > at the start of the transaction they are allowed to read the group > the moment they can't read anymore confirms that the membership was successfully revoked:
async function leaveGroup (groupId) {
await deleteDoc(doc(db, 'groups', groupId, 'members', auth.currentUser.uid))
// Cloud Function removes the membership info
// from the group doc...
await new Promise((resolve) => {
const unsubscribeFromSnapshot = onSnapshot(
doc(db, 'groups', groupId),
() => { }, // success callback
() => { // error callback
// membership info is not in the group anymore
// > user can't read the doc anymore
// > transaction was successful
// read access was revoked > transaction was successful:
unsubscribeFromSnapshot()
resolve()
}
)
})
}
B) User is not a member and wants to join the group > at the start of the transaction they are allowed to read the group > the moment they can read the group confirms that the membership was successfully confirmed (this is a simplified version that does not check the invite code):
async function joinGroup (groupId) {
try {
await setDoc(
doc(db, 'groups', groupId, 'members', auth.currentUser.uid),
{
userId: auth.currentUser.uid,
userDisplayName: auth.currentUser.displayName
}
)
// Cloud Function adds the membership
// information to the group doc ...
await new Promise((resolve) => {
let maxRetries = 10
const interval = setInterval(async () => {
try {
const docSnap = await getDoc(doc(db, 'groups', groupId))
if (docSnap.data().members.includes(auth.currentUser.uid)) {
// membership info is in the group doc
// > transaction was successful
clearInterval(interval)
resolve()
}
} catch (error) {
if (maxRetries < 1) {
clearInterval(interval)
}
}
maxRetries--
}, 2000)
})
}
Note: I went with polling here, but similar to what #samthecodingman suggested, another solution could be that the Cloud Function confirms the membership by writing back to the members document (which the user can always read) and you listen to snapshot changes on this document.
C) Most straightforward way: someone else (the group owner) removes a member from the group > they have read access through the whole transaction > directly listen to snapshot changes:
async function endMembership (groupId, userId) {
await deleteDoc(doc(db, 'groups', groupId, 'members', userId))
// Cloud Function removes the membership info
// from the group doc...
await new Promise((resolve) => {
const unsubscribe = onSnapshot(doc(db, 'groups', groupId), (doc) => {
if (!doc.data().members.includes(userId)) {
// membership info is not in the group doc anymore
// > transaction was successful
unsubscribe()
resolve()
}
})
})
}
In any case you should do proper error handling that covers other causes. I left them out to demonstrate how to use the error handlers when waiting for gaining/loosing read access.
I have a firebase function that runs based on a trigger to create a chat room. The function was running for a long time correctly until I updated it to add a new field on the document it is creating. That is archived: false.
After adding the field on the function, it sometimes adds the field when it runs but at times fails to add the field. So I think that firebase at times runs the updated code and sometimes runs the only code when the function is triggered because all other fields are created apart from the archived: false which is created and sometimes not created.
So I fail to understand why this happens yet in the code, the field archived is not dependent on any other variable.
Below is the function.
// firestore trigger to crete a chat room
exports.createChatRooms = functions.firestore.document("/jobs/{id}").onUpdate((change, context) => {
const job = change.after.data();
if (job.status === "Accepted") {
const roomId = context.params.id;
const room = admin.firestore().collection("rooms").doc(roomId);
return room.set({
name: job.title,
jobId: roomId,
createdAt: new Date().getTime(),
agent: job.agent,
archived: false,
user: job.user,
members: [job.user.id, job.agent.id],
});
} else {
return null;
}
});
Kindly help me understand why this is happening.
How do you check whether a user is logged in via third party (Google, Facebook, ...) in the Meteor framework? Also, is this possible from the client?
There are multiple ways to do it. On the Server side you would have a function like Accounts.onCreateUser((options, user) => {... }).
If you already publish minimum data of the user, you can add a key using onCreateUser and save something like: loginVia: "email" or "FB" etc. Then you publish that key or get its value with a method.
The straight forward solution is to check if the social service exists if look for a particular service.
For Example:
const isFBUser: Meteor.users.find({ _id :.... }, { 'services.facebook': { $exists: true } }).count() // results in 1 record or 0 records = true / false
of if you want to know if the user is coming via email and not third party you can check for emails
const isThirdParty = Meteor.users.find({_id: ...}, emails: { $exists: true })
It is pretty common to also use a merge accounts system so that someone coming from FB with the email gigi#gmail.com will letter be allowed to log in to you app with the email instead of the social account. In this case, you would need to eventually save the source of the last login.
I'll leave here for you part of my onCreateUser as example of how to pull data out of a 3rd party user and save it in the use profile. On the same lines you can save the 3rd party source (as suggested above)
if (user.services) {
const fb = user.services.facebook
const google = user.services.google
let avatar = null
let fbi = null // I use this to keep a record of the FB user Id
let ggli = null // // I use this to keep a record of the Google user Id
if (fb) {
/**
* I upload to S3 and I don't wait for a response. A little risky...
*/
put_from_url(`https://graph.facebook.com/${fb.id}/picture?width=500&height=500`, `avatar/${fb.id}.jpg`, (err, res) => {
if (err) {
console.log('Could not upload FB photo to S3, ', err)
} else {
// console.log(res)
}
})
user.profile = extend(user.profile, {
firstName: fb.first_name,
lastName: fb.last_name,
email: fb.email,
displayName: fb.name,
gender: startCase(toLower(fb.gender)),
avatar: `${fb.id}.jpg`
})
avatar = `${fb.id}.jpg`
fbi = fb.id
roles = ['user', 'social']
}
if (google) {
/**
* I upload to S3 and I don't wait for a response. A little risky...
*/
put_from_url(google.picture + '?sz=500', `avatar/${google.id}.jpg`, err => {
if (err) {
console.log('Could not upload Google photo to S3, ', err)
}
})
user.profile = extend(user.profile, {
firstName: google.given_name,
lastName: google.family_name,
email: google.email,
displayName: google.name,
gender: startCase(toLower(google.gender)),
avatar: `${google.id}.jpg`
})
avatar = `${google.id}.jpg`
ggli = google.id
roles = ['user', 'social']
}
/**
* Create a slug for each user. Requires a display name for all users.
*/
let slug
slug = Meteor.call('/app/create/slug', user.profile.displayName, 'user')
Also please check the user object structure:
And check this out. Users via 3rd party don't have the email field so you can check its existence.
Due to my probable misuse of anonymous authentication (see How to prevent Firebase anonymous user token from expiring) I have a lot of anonymous users in my app that I don't actually want.
I can't see any way to bulk delete these users. Do I have to do it manually one-by-one? Is there anyway to use the API to access user accounts and manipulate them for users other than the current user?
This code sample uses the Firebase Admin SDK for Node.js, and will delete any user that has no providerData, which means the user is anonymous:
function deleteAnonymousUsers(nextPageToken) {
adminApp
.auth()
.listUsers(20, nextPageToken)
.then(function(listUsersResult) {
listUsersResult.users.forEach(function(userRecord) {
if (userRecord.providerData.length === 0) { //this user is anonymous
console.log(userRecord); // do your delete here
adminApp.auth().deleteUser(userRecord.uid)
.then(function() {
console.log("Successfully deleted user");
})
.catch(function(error) {
console.log("Error deleting user:", error);
});
}
});
if (listUsersResult.pageToken) {
// List next batch of users.
deleteAnonymousUsers(listUsersResult.pageToken);
}
})
.catch(function(error) {
console.log('Error listing users:', error);
});
}
There is no way in the Firebase Console to bulk-delete users.
There is no API to bulk-delete users.
But there is administrative API that allows you to delete user accounts. See https://firebase.google.com/docs/auth/admin/manage-users#delete_a_user
I just wanted to add a method I just used to (sort-of) bulk-delete. Mostly because I felt clever after doing it and I am not that clever.
I downloaded a mouse-automation application that lets you record your mouse clicks then replay it automatically. I just deleted almost 1000 users while playing the piano lol.
I used Macro Recorder and it worked like a charm. Just recorded a few iterations in the console of me deleting users, set it to repeat 500 times and walked away.
I know this isn't a very technical answer, but it saved me hours of monotonous mouse clicking so hopefully someone else looking for a way to bulk-delete will benefit from it as well. I hated the fact that there was no bulk-delete and really needed a way out of it. It only took about 20 manual deletes to realize there were apps that could do what I was doing.
If you do not need to do it on a large scale and you want to delete some anonymous users from Firebase Console UI, but you are lazy to click on 250 users one-by-one, run the following code in your console (screen where table with users is shown):
rows = Array.from(document.querySelectorAll('td.auth-user-identifier-cell')).map(td => td.parentNode).filter((tr) => tr.innerText.includes('anonymous'))
var nextTick = null
function openContextMenu(tr) {
console.log('openning menu')
tr.querySelector('.edit-account-button').click()
nextTick = deleteUser
}
function deleteUser() {
console.log('deleting user')
document.querySelector('.cdk-overlay-connected-position-bounding-box button:last-of-type').click()
nextTick = confirmDelete
}
function confirmDelete() {
console.log('confirming action')
document.querySelector('.cdk-global-overlay-wrapper .confirm-button').click()
nextTick = getUser
}
function getUser() {
console.log('getting user')
openContextMenu(rows.shift())
}
nextTick = getUser
step = 500
setInterval(() => {
nextTick()
}, step)
It basically selects all rows which contain anonymous user and simulate you clicking the three dots, then clicking on delete account and as a last step it confirms action in the modal which appears.
Before running the script, select 250 rows per page in the table's footer. When all anonymous users are removed, you must manually go to next page and re run the script (or code in another "tick" which paginates for you).
It takes 1.5 second to delete one user (you can modify this with step variable, but I do not recommend go lower than 500ms - mind the UI animations).
It runs also in a tab in background so you can watch some YT in meantime :)
Update 2021:
I had around 10,000 anonymous users, and #regretoverflow's solution lead to exceeding the delete user quota. However, slightly tweaking the code to utilize the admin's deleteUsers([userId1, userId2, ...]) API works like a charm.
function deleteAnonymousUsers(nextPageToken: string | undefined) {
firebaseAdmin
.auth()
.listUsers(1000, nextPageToken)
.then(function (listUsersResult) {
const anonymousUsers: string[] = [];
listUsersResult.users.forEach(function (userRecord) {
if (userRecord.providerData.length === 0) {
anonymousUsers.push(userRecord.uid);
}
});
firebaseAdmin
.auth()
.deleteUsers(anonymousUsers)
.then(function () {
if (listUsersResult.pageToken) {
// List next batch of users.
deleteAnonymousUsers(listUsersResult.pageToken);
}
})
})
}
deleteAnonymousUsers(undefined);
There is a firebase-functions-helper package, that can help to delete firebase users in bulk.
// Get all users
firebaseHelper.firebase
.getAllUsers(100)
.then(users => {
users.map(user => {
firebaseHelper.firebase
.deleteUsers([user.uid]);
})
})
The code above will get 100 users, and delete all of them. If you don't pass the number, the default value is 1000. You can read the instruction on Github repository.
I faced the same problem today then I found Firebase Admin SDK. I am using Node.js which is very easy to install, so you can try the following code. It is not a complete answer I know but one can build its own script/application to delete stored uids. Yet, there is no way to retrieve a list, so you have to build one somehow every time you create an anonymous account.
First, download your 'serviceAccountKey.json' which can be done through the Firebase Console (Project Settings). In my case I renamed the download file to a more friendly name and saved to documents folder.
console.firebase.google.com/project/yourprojectname/settings/serviceaccounts/adminsdk
Useful links:
Firebase Admin SDK Setup
Firebase Admin User Management
Firebase Admin Database API
Then, play around using Windows cmd.exe or any other shell. The 'npm install -g' installs firebase-admin globally in your machine.
$ npm install firebase-admin -g
$ node
> var admin = require("firebase-admin");
> admin.initializeApp({
credential: admin.credential.cert("./documents/yourprojectname-firebase-admin.json"),
databaseURL: "https://yourprojectname.firebaseio.com"
});
> var db = admin.database();
// Of course use an existent UID of your choice
> admin.auth().getUser('2w8XEVe7qZaFn2ywc5MnlPdHN90s').then((user) => console.log
(user))
> admin.auth().deleteUser('2w8XEVe7qZaFn2ywc5MnlPdHN90s').then(function() {
console.log("Successfully deleted user");
}).catch(function(error) {
console.log("Error deleting user:", error);
});
// To get access to some key/values in your Database:
> var ref = db.ref("users/1234");
> ref.once("value", function(snapshot) {
console.log(snapshot.val());
});
I was writing myself a firebase functions function with Firebase auth.
It works like a charm for me and i can clean with one API call.
// Delete all Anon User
exports.deleteUser = functions.https.onRequest(async (req, res) => {
const admin = require("firebase-admin");
//initialize auth
admin.initializeApp();
//create auth instance
const auth = admin.auth();
//Get the list of all Users
const allUsers = await auth.listUsers();
//Identify the Anon User give other user null
const allUsersUID = allUsers.users.map((user) => (user.providerData.length === 0) ? user.uid : null);
//remove the null
const filteredallUsersUID = allUsersUID.filter(e => e !== null)
//delete and answer the API call
return auth.deleteUsers(filteredallUsersUID).then(() => res.send("All Anon-User deleted"));
});
With this you can just simply call your API URL
https://[Your_API_URL]/deleteUser
Just require basic knowledge of Firebase Functions.
I assume this could be also added to a cron job.
I had the same problem. because Firebase doesn't provide any API to delete bulk users but this is how I have deleted all anonymous users.
Download all the users as json via firebase tool
firebase auth:export users --format=json
https://firebase.google.com/docs/cli/auth#file_format
You can write a firebase cloud function to trigger or write a action method to trigger
import the json file in to your file,
const Users = require('./users.json'); // ES5 <br>
import Users from './users.json'); // ES6 <br>
normally anonymous user doesn't have email so it is easy to delete the record which doesn't have email id
Users.users.map(user => {
setTimeout(() => {
admin.auth().deleteUser(user.localId).then(() =>{
console.log("Successfully deleted user");
})
.catch((error) => {
console.log("Error deleting user:", error);
});
}, 20000);
});
Don't try to reduce the timeout second. It will throw below error
Error deleting user: { Error: www.googleapis.com network timeout. Please try again.
The Firebase Admin SDK can also delete multiple users at once.
Here is Node.js sample.
admin.auth().deleteUsers([uid1, uid2, uid3])
.then(deleteUsersResult => {
console.log('Successfully deleted ' + deleteUsersResult.successCount + ' users');
console.log('Failed to delete ' + deleteUsersResult.failureCount + ' users');
deleteUsersResult.errors.forEach(err => {
console.log(err.error.toJSON());
});
})
.catch(error => {
console.log('Error deleting users:', error);
});
Notice: there is a limitation as list all users.
The maximum number of users allowed to be deleted is 1000 per batch.
I am creating an application that requires a simple user registration form. I am using account:core package to create users but my challenge is that the user gets created without any form of reply to the user that the account was successfully created. Here is my code
Template.register.events({
'submit form': function (event) {
event.preventDefault();
Accounts.createUser({
email: $('[name=email]').val(),
password: $('[name=password]').val(),
profile: {
first_name: $('[name=firstname]').val(),
last_name: $('[name=lastname]').val(),
current_location: $('[name=currentlocation]').val(),
date: $('[name=date]').val(),
phone_number: $('[name=phonenumber]').val(),
}
});
},
});
Accounts.createUser(options, [callback]) accepts a callback as the second parameter, which gets called once the account creation is completed. In this callback you can post your message to the user.
See the docs for more here.