I do the tutorial from CodeLab his project with all steps are here Github. Codelab helped me lot, Thanks!
I do the steps and deleted all IAP products from tutorial and added only subscribable products. i have two purchase product "Normal" and "Ultimate" in the same Family in Appstoreconnect. Its working well, but I found a problem:
Situation A:
When the user subscribed one from them its working all fine, but when the user want to subscribe the other like from "Normal" to "Ultimate" or "Ultimate" to "Normal" in his Validate activ time, then Firebase Cloud don't update his Purchase (Produkt ID and Order ID Is still the old ID's). When Firebase don't update, then get the user not his upgrade to other subscribe instantly. He get there upgrade to the other purchase after a year.
Situation B:
The same Problem, but Outside from the App.
User subscribed one product, then he go outside from app in his Appstore settings and change his subscribe product. Firebase get a info from Apple, but Firebase Cloud don't update the subscription information from User.
can u or have u solve this problem?
my changes from Codelab ->
Constant.dart
const cloudRegion = 'europe-west1';
const subscriptionList = ["kunde_1_fahrzeug", "kunde_3_fahrzeug"];
//storeKeySubscription
const subscription_kunde_1_fahrzeug = 'kunde_1_fahrzeug';
const subscription_kunde_3_fahrzeug = 'kunde_3_fahrzeug';
IAPRepo.dart
void updatePurchases() {
// omitted
// hasActiveSubscription = purchases.any((element) => element.productId == subscription_kunde_1_fahrzeug && element.status != Status.expired);
//hasActiveSubscription = purchases.any((element) => element.productId == subscriptionList && element.status != Status.expired);
hasActiveSubscription = purchases.any((element) => subscriptionList.any((x) => x == element.productId) && element.status != Status.expired);
for(PastPurchase x in purchases){
print("Gelb hasActiveSubscription IAP-REPO : ${x.productId} - ${x.status}");
};
hasUpgrade = purchases.any(
(element) => subscriptionList.any((x) => x == element.productId),
);
/*
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
*/
notifyListeners();
// omitted
}
XXX_Purchase
void purchasesUpdate() {
// omitted
if (products.isNotEmpty) {
// subscriptions = products .where((element) => element.productDetails.id == subscription_kunde_1_fahrzeug) .toList();
subscriptions = products
.where((element) => subscriptionList.any((x) => x == element.productDetails.id))
.toList();
upgrades = products
.where((element) => subscriptionList.any((x) => x == element.productDetails.id))
.toList();
}
// omitted
}
Future<void> loadPurchases() async {
// omitted
const ids = <String>{
subscription_kunde_1_fahrzeug,
subscription_kunde_3_fahrzeug,
//storeKeyUpgrade,
};
// omitted
}
Future<void> buy(PurchasableProduct product) async {
// omitted
// case storeKeyConsumable:
// await iapConnection.buyConsumable(purchaseParam: purchaseParam);
// break;
case subscription_kunde_1_fahrzeug:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
case subscription_kunde_3_fahrzeug:
//case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
// omitted
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
// omitted
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case subscription_kunde_1_fahrzeug:
print("Orange: ID Produkt: ${purchaseDetails.productID}, ${purchaseDetails.transactionDate}, ${purchaseDetails.verificationData}, ${purchaseDetails.status}, ${purchaseDetails.purchaseID}, ${purchaseDetails.pendingCompletePurchase}, switch (purchaseDetails.productID) case: subscription_kunde_1_fahrzeug");
counter.applyPaidMultiplier_kunde_1_fahrzeug();
break;
case subscription_kunde_3_fahrzeug:
print("Orange: ID Produkt: ${purchaseDetails.productID}, ${purchaseDetails.transactionDate}, ${purchaseDetails.verificationData}, ${purchaseDetails.status}, ${purchaseDetails.purchaseID}, ${purchaseDetails.pendingCompletePurchase}, switch (purchaseDetails.productID) case: subscription_kunde_3_fahrzeug");
counter.applyPaidMultiplier_kunde_3_fahrzeug();
break;
// case storeKeyConsumable:
// counter.addBoughtDashes(2000);
// break;
/* case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
*/
// omitted
}
PastPurchase
#immutable
class PastPurchase {
// omitted
String get title {
switch (productId) {
case subscription_kunde_1_fahrzeug:
return 'Subscription';
case subscription_kunde_3_fahrzeug:
return 'Subscription';
default:
return productId;
}
}
// omitted
}
Firebase Backend
export interface ProductData {
productId: string;
type: "SUBSCRIPTION" | "NON_SUBSCRIPTION";
}
export const productDataMap: { [productId: string]: ProductData } = {
"kunde_1_fahrzeug": {
productId: "kunde_1_fahrzeug",
type: "SUBSCRIPTION",
},
"kunde_3_fahrzeug": {
productId: "kunde_3_fahrzeug",
type: "SUBSCRIPTION",
},
};
The problem is the codelab is unrealistically simplistic related to subscriptions, and relies on a node.js package that also doesn't handle in-family subscription changes. Subscription changes with apple don't provide the new product_id, they provide the new subscription id as auto_renew_product_id, and the product_id stays the same from the original transaction. Turn verbose: true to see it when running your function.
So, to fix, you'd need a third function for in-app subscription changes, which you can't validate and return properly from apple-receipt-verify because that package doesn't provide the auto_renew_product_id that you're switching to. So you'll need a new way to validate the receipt.
For changes outside the app, you'll need to fix the handleServerEvent, because that doesn't work to change subscriptions from outside app.
I suggest RevenueCat. The cost is minimal, and you'll have a company with a vested interest in updating the API's.
Your revenue foundation needs to be firm, and while the Google Play Store performance of the package and server-side code was spotless for me...their interaction with Apple is barely functional for the simplest of scenarios.
Edit: A couple points to clarify, expound on for future readers.
There is a typo as of May 2022 in the code lab. You need to use Type 1 Notifications from Apple.
Apple handles in-group subscription changes itself. It will unsubscribe and subscribe within a group. You also need to rank your in-group subscriptions with apple. Android just changes right away.
Apple handles down-grade, cross-grade and upgrade differently. All downgrades must finish their term before changing. This prevents apple from having to refund. So when you downgrade, the term must finish, then apple will change the sub and fire the server event.
The code lab logic has Android creating/canceling subs and leaving a sub history trail of purchases documents in firestore...this is because every sub change is a cancel and create. Apple however changes the subs internally, and then fires to the server telling you what changed. So the code lab does not have a doc trail detailing any subscription history..each sub is one doc always...just changes product id and status.
In short: doing the codelab, and getting to a functional level for your app will definitely give you a good understanding of how each store handles subs...and how differently they handle them. As it relates to the in_app_purchase plug-in, server side validation and performance...while elegantly done, it was the bare minimum guidance given by the codelab and package.
Related
A firebase cloud function has been implemented to be triggered every time the value of price changes, the code is as follows:
export const orderListener = functions.firestore
.document("/users/{userId}/Order/{orderId}")
.onUpdate(async (change, context) => {
const { userId, orderId } = context.params;
const price = change.after.data().price;
try {
if (price !== 0) {
const userDoc = await admin
.firestore()
.collection(`/users/${userId}/Personal information`)
.doc("Personal Information")
.get();
const { fcmToken } = userDoc.data()!;
functions.logger.log({
userId,
fcmToken,
orderId,
price,
});
//RETURN
return admin.messaging().sendToDevice(fcmToken, {
notification: {
title: "Title of the message",
body: `${orderId}'s price > 0`,
},
});
} else {
return null
}
} catch (e) {
functions.logger.error(e.message);
//RETURN
return null
This code has been deployed in the firebase function, and I tried to test it by changing the value of price, and monitoring execution by looking at firebase console.
Expectation:
As soon as I change the value of price in my firestore database, it executes almost immediately (within 5s).
Result:
The function executes just fine and was logged on the firebase console, but there is a 50 minutes delay in execution after I updated the price value (Function execution was only logged 50mins later). It was tested by someone else but does not seem to have this issue.
Question:
Any potential error contained within this code? Is it likely to be some error with the firebase setup, settings, etc...?
Edit: Thanks to the contribution of Dharmaraj I have updated the above code with return statements. The problem however still persists.
You should consider two things here:
1 - Cold Start of functions. If a functions is update or hasen't been used for a while it will kind of "shut down" or "go to sleep". When it gets triggered again it has a "cold start" that takes a while. It kind of boots the function from the "sleep"
2 - The loggs are not in real time. The loggs are not shown in realtime. They also have some lag and deppending if you are watching them on the Firebase Console or GCP console you may need to refresh them manually.
50 min is way to much for a cloud function. I belive that you maybe just haben't seen the log as you expected it. It also could be that you test device was offline while trying it and it synced later. Without more information of your code, device and how you tested it it's hard for us to tell what exactly it would be but the mentioned szenarios are the most likely ones.
I'm attempting to get a list of items from a MongoDB Atlas instance via Realm into my Xamarin Forms application. I'm pretty sure I've followed the instructions correctly, however I can't seem to see any values come in.
I'm using Realm and Realm.Fody version: 10.2.0
I've uploaded my data with the following script
mongoimport --uri mongodb+srv://[user]:[pass]#cluster0.[wxyz].mongodb.net/[company] --collection products --type json --file products --jsonArray
1057 document(s) imported successfully. 0 document(s) failed to import.
When I go to MongoDB Atlas, I see this
In my App.xaml.cs constructor, I create a realm app and log in with anonymous credentials
public const string MasterDataPartitionKey = "master_data";
ctor()
{
var app = Realms.Sync.App.Create("my-app-id");
app.LogInAsync(Credentials.Anonymous()).ContinueWith(task => User = task.Result);
}
After this, in my first ViewModel constructor (it's Rx, but isn't doing anything fancy)
ctor()
{
_listProductsCommand = ReactiveCommand.Create<Realm, IEnumerable<Product>>(ExecuteLoadProducts);
_listProductsCommand.ThrownExceptions.Subscribe(x => { /* checking for errors here */ }).DisposeWith(TrashBin);
Initialize
.ObserveOn(RxApp.MainThreadScheduler)
// note MasterDataPartitionKey is the value I've set on every single record's `_partitionKey` property.
.Select(_ => new SyncConfiguration(MasterDataPartitionKey, App.User))
.SelectMany(Realm.GetInstanceAsync)
.Do(_ => { }, ex => { /* checking for errors here */ })
.ObserveOn(RxApp.MainThreadScheduler)
.InvokeCommand(this, x => x._listProductsCommand)
.DisposeWith(TrashBin);
}
And later, a simple query
private static IEnumerable<Product> ExecuteLoadProducts(Realm realm)
{
var data = realm.All<ProductDto>();
var productDtos = data.ToList();
var products = productDtos.Select(x => x.Map());
return products;
}
Expected:
productDtos (and products) should have a count of 1057
Actual:
productDtos (and products) have a count of 0
I've looked in my Realm logs and I can see the connections and sync logs
My anonymous authentication is turned on
and I've made it so that all of my Collections have read access (in fact developer mode is on)
Here's an example of one of the records
Here's a snipped of the dto I'm trying to pull down
I feel as though I must be missing something simple. Can anyone see anything obvious that could be sending me sideways?
Sorry for the poorly worded title, I'll try to explain as best as I can. I am creating a role shop command using the new discord-buttons module, and came across a problem, to my understanding I would have to create a button for each individual role, in order for someone to buy it. After searching through documentation, I'm still a bit stumped. Here's some example code I put together to show what I'm trying to do:
let embedRed = new Discord.MessageEmbed()
.setTitle('Red Role')
.setColor('#c46413')
.addField('**Price**', '10,000', true)
.addField('**Color Hex:**', '#ffffff',true)
let embedBlue = new Discord.MessageEmbed()
.setTitle('Blue')
.setColor('#c2a289')
.addField('**Price**', '10,000', true)
.addField('**Color Hex:**', '#ffffff',true)
///Buttons
let buttonBuyRed = new MessageButton()
.setStyle('green')
.setLabel('Buy Red Role')
.setID('role_buy1')
let buttonBuyBlue = new MessageButton()
.setStyle('green')
.setLabel('Buy Blue Role')
.setID('role_buy2')
//embeded messages being sent
message.channel.send({ buttons: [buttonBuyRed], embed: embedRed});
message.channel.send({ buttons: [buttonBuyRed], embed: embedBlue});
//What happens if buttons are pressed
client.on('clickButton', async (role_buy1) => {
if (button.id === 'roley_buy1') {
button.channel.send(`${button.clicker.user.tag} bought red role`);
db.push(message.author.id, `${message.guild.roles.cache.get('role id here')}`) //role being pushed to user's inventory
}
});
client.on('clickButton', async (role_buy2) => {
if (button.id === 'role_buy2') {
button.channel.send(`${button.clicker.user.tag} bought blue role`);
db.push(message.author.id, `${message.guild.roles.cache.get('role id here')}`) //role being pushed to user's inventory
}
});
Since I have about 25 different roles that I want users to be able to purchase, it's quite a hassle to create a button for each role, I am looking for a way to just use one single "buy_role" button that works for all available roles.
If I didn't explain something clearly, please let me know, any help is appreciated!
So i came to a conclusion, this code works, but if your guild has a lot of roles, it would throw an error "Invalid form body"
const rolesInGuild = message.guild.roles.cache.array(); //creating array from collection of roles in a guild
const buttons = []; // an empty array for our buttons
for (const role of rolesInGuild) { // creating a loop inorder to create a button for every roles in rolesInGuild Array
const button = new MessageButton()
.setStyle('red') // default: blurple
.setLabel(`${role.name}`) // default: NO_LABEL_PROVIDED
.setID(`${role.id}`);
buttons.push(button); // button id is the same as role id so its unique!
}
console.log(rolesInGuild);
console.log(buttons);
await message.channel.send('test', { buttons: buttons }); // sending our buttons
bot.on('clickButton', async(button) => {
for (const btn of buttons) {
if (btn.custom_id == button.id) {
const role = button.guild.roles.cache.get(btn.custom_id);
const member = message.guild.members.cache.get(button.clicker.user.id);
member.roles.add(role);
}
}
});
you could add specific roles to the array rolesInGuild in this format
[{ name: 'rolename', id: 'roleid' }] instead of every roles in the guild ( I wasn't sure what your goal was)
you have ${message.guild...}, that’s the wrong if you have an error, so try this:
client.on('clickButton', async (button) => {
if (button.id === 'roley_buy1') {
button.channel.send(`${button.clicker.user.tag} bought red role`);
db.push(message.author.id, `${button.guild.roles.cache.get('role id here')}`)
//role being pushed to user's inventory
button.clicker.roles.add('your role id');
// or you can find the role using
const role = button.guild.roles.cache.find(role => role.name == 'rolename');
button.clicker.roles.add(role);
}
});```
I am developing a PWA, which displays a list of transactions (transaction is an object with ~10 fields). I am using firestore for storage and realtime updates and I have also enabled persistance.
I want my application to have all the data in memory and I want to take care of displaying only necessary information myself (e.g. using virtual scrolling for transaction list). Due to this reason I listen to the whole collection (a.k.a the transactions).
At the start of the app, I want to make sure the data is loaded so I use one time cache query to get the transactions. I would expect the query to be nearly instantaneous, but on laptop it takes around ~1 second to get the initial data (and I also have another collection which I fetch from cache and this resolves after ~2 seconds after transactions request). For mobile it takes around ~9seconds (loading on mobile, loading on laptop)
I want my app to feel instantaneous, but I takes a few seconds until the data is in place. Note, that I am not doing any advanced queries (I just want to load the data to memory).
Am I doing something wrong? I have read Firestore docs, but I don't think the amount of data that I have in cache should cause such bad performance.
UPDATE: Even if I limit the initial query to just load 20 documents. It still takes around ~2 seconds to retrieve them.
UPDATE 2: The code looks like this:
export const initializeFirestore = (): Thunk => (dispatch) => {
const initialQueries: Array<Promise<unknown>> = []
getQueries().forEach((query) => {
const q = query.createFirestoneQuery()
initialQueries.push(
q
.get({
source: 'cache',
})
.then((snapshot) =>
dispatch(firestoneChangeAction(query, snapshot, true)),
),
)
q.onSnapshot((change) => {
dispatch(firestoneChangeAction(query, change))
})
})
console.log('Now I am just waiting for initial data...')
return Promise.all(initialQueries)
}
You may be interested by the smart approach presented by Firebase engineers during the "Faster web apps with Firebase" Session of the Firebase Summit 2019 (You can watch the video here: https://www.youtube.com/watch?v=DHbVyRLkX4c).
In a nutshell, their idea is to use the Firestore REST API to make the first query to the database (which does not need to download any SDK), and in parallel, dynamically import the Web SDK in order to use it for the subsequent queries.
The github repository is here: https://github.com/hsubox76/fireconf-demo
I paste below the content of the key js file (https://github.com/hsubox76/fireconf-demo/blob/master/src/dynamic.js) for further reference.
import { firebaseConfigDynamic as firebaseConfig } from "./shared/firebase-config";
import { renderPage, logPerformance } from "./shared/helpers";
let firstLoad = false;
// Firestore REST URL for "current" collection.
const COLLECTION_URL =
`https://firestore.googleapis.com/v1/projects/exchange-rates-adcf6/` +
`databases/(default)/documents/current`;
// STEPS
// 1) Fetch REST data
// 2) Render data
// 3) Dynamically import Firebase components
// 4) Subscribe to Firestore
// HTTP GET from Firestore REST endpoint.
fetch(COLLECTION_URL)
.then(res => res.json())
.then(json => {
// Format JSON data into a tabular format.
const stocks = formatJSONStocks(json);
// Measure time between navigation start and now (first data loaded)
performance && performance.measure("initialDataLoadTime");
// Render using initial REST data.
renderPage({
title: "Dynamic Loading (no Firebase loaded)",
tableData: stocks
});
// Import Firebase library.
dynamicFirebaseImport().then(firebase => {
firebase.initializeApp(firebaseConfig);
firebase.performance(); // Use Firebase Performance - 1 line
subscribeToFirestore(firebase);
});
});
/**
* FUNCTIONS
*/
// Dynamically imports firebase/app, firebase/firestore, and firebase/performance.
function dynamicFirebaseImport() {
const appImport = import(
/* webpackChunkName: "firebase-app-dynamic" */
"firebase/app"
);
const firestoreImport = import(
/* webpackChunkName: "firebase-firestore-dynamic" */
"firebase/firestore"
);
const performanceImport = import(
/* webpackChunkName: "firebase-performance-dynamic" */
"firebase/performance"
);
return Promise.all([appImport, firestoreImport, performanceImport]).then(
([dynamicFirebase]) => {
return dynamicFirebase;
}
);
}
// Subscribe to "current" collection with `onSnapshot()`.
function subscribeToFirestore(firebase) {
firebase
.firestore()
.collection(`current`)
.onSnapshot(snap => {
if (!firstLoad) {
// Measure time between navigation start and now (first data loaded)
performance && performance.measure("realtimeDataLoadTime");
// Log to console for internal development
logPerformance();
firstLoad = true;
}
const stocks = formatSDKStocks(snap);
renderPage({
title: "Dynamic Loading (Firebase now loaded)",
tableData: stocks
});
});
}
// Format stock data in JSON format (returned from REST endpoint)
function formatJSONStocks(json) {
const stocks = [];
json.documents.forEach(doc => {
const pathParts = doc.name.split("/");
const symbol = pathParts[pathParts.length - 1];
stocks.push({
symbol,
value: doc.fields.closeValue.doubleValue || 0,
delta: doc.fields.delta.doubleValue || 0,
timestamp: parseInt(doc.fields.timestamp.integerValue)
});
});
return stocks;
}
// Format stock data in Firestore format (returned from `onSnapshot()`)
function formatSDKStocks(snap) {
const stocks = [];
snap.forEach(docSnap => {
if (!docSnap.data()) return;
const symbol = docSnap.id;
const value = docSnap.data().closeValue;
stocks.push({
symbol,
value,
delta: docSnap.data().delta,
timestamp: docSnap.data().timestamp
});
});
return stocks;
}
You're not doing anything wrong. The query will take as much time as it needs to finish. This is why many sites use a loading indicator.
For the first query in your app, it's going to include the time it takes to fully initialize the SDK, which might involve asynchronous work beyond more than just the query itself. Also bear in mind that reading and sorting data from local disk isn't necessarily "fast", and that for larger amounts of documents, the local disk cache read might even be more expensive than the time it would take the fetch the same documents over the network.
Since we don't have any indication of how many documents you have, and how much total data you're trying to transfer, and the code you're using for this, all we can do is guess. But there's really not much you can do to speed up the initial query, other than perhaps limiting the size of the result set.
If you think that what you're experiencing is a bug, then please file a bug report on GitHub.
I build react native app with firebase & firestore.
what I'm looking to do is, when user open app, to insert/update his status to 'online' (kind of presence system), when user close app, his status 'offline'.
I did it with firebase.database.onDisconnect(), it works fine.
this is the function
async signupAnonymous() {
const user = await firebase.auth().signInAnonymouslyAndRetrieveData();
this.uid = firebase.auth().currentUser.uid
this.userStatusDatabaseRef = firebase.database().ref(`UserStatus/${this.uid}`);
this.userStatusFirestoreRef = firebase.firestore().doc(`UserStatus/${this.uid}`);
firebase.database().ref('.info/connected').on('value', async connected => {
if (connected.val() === false) {
// this.userStatusFirestoreRef.set({ state: 'offline', last_changed: firebase.firestore.FieldValue.serverTimestamp()},{merge:true});
return;
}
await firebase.database().ref(`UserStatus/${this.uid}`).onDisconnect().set({ state: 'offline', last_changed: firebase.firestore.FieldValue.serverTimestamp() },{merge:true});
this.userStatusDatabaseRef.set({ state: 'online', last_changed: firebase.firestore.FieldValue.serverTimestamp() },{merge:true});
// this.userStatusFirestoreRef.set({ state: 'online',last_changed: firebase.firestore.FieldValue.serverTimestamp() },{merge:true});
});
}
after that, I did trigger to insert data into firestore(because I want to work with firestore), this is the function(works fine, BUT it takes 3-4 sec)
module.exports.onUserStatusChanged = functions.database
.ref('/UserStatus/{uid}').onUpdate((change,context) => {
const eventStatus = change.after.val();
const userStatusFirestoreRef = firestore.doc(`UserStatus/${context.params.uid}`);
return change.after.ref.once("value").then((statusSnapshot) => {
return statusSnapshot.val();
}).then((status) => {
console.log(status, eventStatus);
if (status.last_changed > eventStatus.last_changed) return status;
eventStatus.last_changed = new Date(eventStatus.last_changed);
//return userStatusFirestoreRef.set(eventStatus);
return userStatusFirestoreRef.set(eventStatus,{merge:true});
});
});
then after that, I want to calculate the online users in app, so I did trigger when write new data to node of firestore so it calculate the size of online users by query.(it works fine but takes 4-7 sec)
module.exports.countOnlineUsers = functions.firestore.document('/UserStatus/{uid}').onWrite((change,context) => {
console.log('userStatus')
const userOnlineCounterRef = firestore.doc('Counters/onlineUsersCounter');
const docRef = firestore.collection('UserStatus').where('state','==','online').get().then(e=>{
let count = e.size;
console.log('count',count)
return userOnlineCounterRef.update({count})
})
return Promise.resolve({success:'added'})
})
then into my react native app
I get the count of online users
this.unsubscribe = firebase.firestore().doc(`Counters/onlineUsersCounter`).onSnapshot(doc=>{
console.log('count',doc.data().count)
})
All the operations takes about 12 sec. it's too much for me, it's online app
my firebase structure
what I'm doing wrong? maybe there is unnecessary function or something?
My final goals:
minimize time operation.
get online users count (with listener-each
change, it will update in app)
update user status.
if there are other way to do that, I would love to know.
Cloud Functions go into a 'cold start' mode, where they take some time to boot up. This is the only reason I can think of that it would take that long. Stack Overflow: Firebase Cloud Functions Is Very Slow
But your cloud function only needs to write to Firestore on log out to
catch the case where your user closes the app. You can write to it directly on log in from your client
with auth().onAuthStateChange().
You could also just always read who is logged in or out directly from the
realtime database and use Firestore for the rest of your data.
You can rearrange your data so that instead of a 'UserStatus' collection you have an 'OnlineUsers' collection containing only online users, kept in sync by deleting the documents on log out. Then it won't take a query operation to get them. The query's impact on your performance is likely minimal, but this would perform better with a large number of users.
The documentation also has a guide that may be useful: Firebase Docs: Build Presence in Cloud Firestore