How to use push() with onDisconnect when user crashes - swift

Assume I have text messages stored in my database and are ordered by childByAutoId() using timestamps. Something like this:
-messages
- dh28hd82jss
...
- nds28h82h1a
...
- di2jd92dhtd
...
What I am trying to do is push() a new message when user crashes.
This is how I typically use onDisconnect operations:
database.child("messages").onDisconnectUpdateChildValues( *update* ){ (error, ref) in
if let error = error {
print("\(error)")
}
completion(true)
}
How can you use push() to append a new text message when a user crashes, using onDisconnect? (text message: "John Disconnected")

You need to know the exact value to write at the moment you call onDisconnectUpdateChildValues. So what you can do is determine a push key now, and then register to write that upon disconnect:
let key = database.childByAutoId().key
database.child("messages").child(key).onDisconnectUpdateChildValues([
"text": "Hello world"
])

Related

Why User Logs In But is Not Authenticating Sporadically In Firebase?

So I have a weird bug that I can't seem to track down. I'm using firebase functions on the backend and SwiftUI. My login flow goes like this:
User logs in from loginView. The loginView then uses a callback to pass a user to move on to the next View after a user logs in.
After this a user is passed to the View where it calls the firebase functions.
The problem is that every once in a while a user fails authentication. This doesn't happen every time and usually happens when a user has not logged in for 12 hours or more. I thought it may have been a race condition at first but after further investigation decided that it wasn't given the fact that it's using a callback.
Has anyone else experienced anything similar? If so is there any way to fix this?
I have tried making my firebase function stay warm by setting minimum instances to 1 because I initially thought it may be a cold start issue but this hasn't helped.
Any help would be greatly appreciated. Thanks.
On the frontend the code is pulling like so:
FirebaseAuthService().signIn(email: email, password: password) { result, error in
if (error?.occurred) != nil {
self.errorMessage = error!.details
self.state = .failed
self.showErrorAlert = true
return
}
if (localAuthEnabled) {
...... This piece of code works
FirebaseFirestoreService().fetchUser(userID: result?.user.uid ?? "", completion: { user, error
....... This piece of code works.
}
})
}
User is then taken to another view AFTER logging in
This view pulls from 5 or so firebase functions asynchronously (but the user is already logged in by this point). The function that it fails at is as follows
self.function.httpsCallable("api-report").call() { (result, error) in
... It is at times it gives me an auth error inside of this function.
}
I am using this to log out whenever a user put the app in the background or hits the log out button:
func signOut() -> Bool {
do {
try Auth.auth().signOut()
self.session = nil
return true
} catch let err as NSError {
print("Error signing out: %#", err)
return false
}
}
on the backend the report call does the following with the report. This is a large function. I have only added the call to show whats going on.
exports.handler = async (data, context) => {
if (!context.auth) {
console.log("report error context");
console.log(context);
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'by authenticated users.', 'User must be authenticated');
}
}
It seems the call to your backend may be happening just as/after the user is logged out, or their token is being refreshed. If it's the token being refreshed, the client should just retry the call in cases such as this.
You could also check whether the token is about to expire, and force a refresh when that is the case, or delay calling the Cloud Function until the token has refreshed.

How can I catch errors in my firebase function when setting a document fails?

I have a firebase cloud function to create a user document with user data whenever a user registers. How would I return an error when the set() fails? Since this is not an http request (an I don't want to use an http request in this case) I have no response. So how would I catch errors?
export const onUserCreated = functions.region('europe-west1').auth.user().onCreate(async user => {
const privateUserData = {
phoneNumber: user.phoneNumber
}
const publicUserData = {
name: 'Nameless'
}
try
{
await firestore.doc('users').collection('private').doc('data').set(privateUserData);
}catch(error)
{
//What do I put here?
}
try
{
await firestore.doc('users').collection('public').doc('data').set(publicUserData);
}catch(error)
{
//What do I put here?
}
});
You can't "return" an error, since the client doesn't even "know" about this function running, there is nobody to respond to.
You can make a registration collection, and in your function make a document there for the current user (using the uid as the document id). In that document, you can put any information you'd like your user to know (status, errors, etc).
So your clients would have to add a listener to this document to learn about their registration.
In your particular code, I think the error is in doc('users'). I guess you meant doc('users/'+user.uid).
Your catch -block will receive errors that occur on your set -call:
try {
await firestore.doc('users').collection('public').doc('data').set(publicUserData);
} catch (error) {
// here you have the error info.
}

Why is a firestore listener returning .added twice when a single document is added?

I'm using google maps and my users can add a map marker to the map.
The map markers are stored in firestore.
I have a listener that listens in to the database.
Each user can only submit one map marker - so there is only one document for each user in the database. But they can modify the lat and lng of their marker.
My firestore is set out as:
root
- submittedMarkers (collection)
- fireauthID_1 (document)
- field 1 (lat):
- field 2 (lng):
- fireauthID_2 (document)
- field 1 (lat):
- field 2 (lng):
// MapController.swift
override func viewWillAppear(_ animated: Bool){
listener = db.collection("submittedMarkers").addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added){
print("added")
}
if(diff.type == .modified) {
print("modified the document in firestore")
}
}
}
}
At the moment for debugging I only have one user in firestore and one document under submittedMarkers -> {fireauthId}.
When my app loads the single marker that currently exists is added to the map along with a single print statement "added". So that works.
Here is the problem I've encountered.
When a new user registers and gets a fireauth id they can then go and add a marker from the AddMarkerController. When they submit the marker i use an unwind segue to get them back to the MapController. (I don't detach the listener at any stage from the MapController).
// AddMarkerController
docref = db.collection("submittedMarkers").document(currentUserId)
docref?.setData([
"latitude": latitude,
"longitude": longitude,
"date_time": date!,
"distance": dist,
"speed": speed
], options: SetOptions.merge()) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
}
}
But what I have found is that when they have come back to the MapController the if (diff.type == .added) is executed twice and print("added") happens twice even though the currentuser fireauth id is added just the once under the collection submittedMarkers.
And when I print out the diff.document.ID i'm getting both the fireauth id of the user who was already in firestore and the current user who just added a new document.
I suspect I don't quite get then how the listener works. I thought if(diff = .added) would listen to the submittedMarkers collection and only fire once when a new document is added.
I wasn't sure if the issue is:
I don't detach the listener when I leave the MapController -> AddMarkerController?
The listener is in viewWillAppear and not viewDidLoad but from reading the firebase blog it should be in viewWillAppear
Whether it's the snapshot.documentChanges.forEach which is looping through the entire snapshot each time something changes
The first added is the local event that is fired as soon as you call setData(). The second added is the acknowledged event after the server has written the data.
See the events for local changes section in the documentation.
Another reason might be that you fail to unregister your observer. If you register your observer in viewWillAppear, unregister it in viewWillDisappear or viewDidDisappear . If you don't, you'll end up with multiple observers for the same event and thus with multiple invocations.

How to Catch Error When Data is not Sent on Angularfire when adding data to firebase?

Im using angularfire to save data into my firebase. Here is a quick code.
$scope.users.$add({
Name:$scope.username,
Age:$scope.newage,
Contact: $scope.newcontact,
});
alert('Saved to firebase');
I am successful in sending these data to my firebase however how can I catch an error if these data are not saved successfully? Any ideas?
EDIT
So after implementing then() function.
$scope.users.$add({
Name:'Frank',
Age:'20',
Contact: $scope.newcontact,
}).then(function(ref) {
alert('Saved.');
}).catch(function(error) {
console.error(error); //or
console.log(error);
alert('Not Saved.');
});
When connected to the internet. The then() function is fine. It waits for those data to be saved in firebase before showing the alert.
What I want is for it to tell me that data is not saved. catch error function is not firing when i am turning off my internet connection and submitting those data.
When you call $add() it returns a promise. To detect when the data was saved, implement then(). To detect when saving failed, implement catch():
var list = $firebaseArray(ref);
list.$add({ foo: "bar" }).then(function(ref) {
var id = ref.key;
console.log("added record with id " + id);
list.$indexFor(id); // returns location in the array
}).catch(function(error) {
console.error(error);
});
See the documentation for add().
Update
To detect when the data cannot be saved due to not having a network connection is a very different problem when it comes to the Firebase Database. Not being able to save in this case is not an error, but merely a temporary condition. This condition doesn't apply just to this set() operation, but to all read/write operation. For this reason, you should handle it more globally, by detecting connection state:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
By listening to .info/connected your code can know that the user is not connected to the Firebase Database and handle it according to your app's needs.

Swift + Locksmith: Not storing value

I am trying to store a value at the end of a game.
I am using the https://github.com/matthewpalmer/Locksmith updateData method.
It says on the Github Repo
as well as replacing existing data, this writes data to the keychain if it does not exist already
try Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount")
This is what I have:
let dictionary = Locksmith.loadDataForUserAccount("gameKeyChainValues")
print("Ended \(EndDateAndTimeString)")
do {
try Locksmith.updateData(["BattleEnd": "12345678"], forUserAccount: "gameKeyChainValues")
}
catch {
print("Unable to set time")
}
print("This line ran")
if let timeBattleEnded = dictionary!["BattleEnd"] as? String {
print("Stored for END: \(timeBattleEnded)")
}
This line print("Ended (EndDateAndTimeString)") outputs:
Ended 19:33:38+2016-08-05
This line print("Unable to set time") does nothing
This line print("This line ran") outputs:
This line ran
This line: print("Stored for END: (timeBattleEnded)") does nothing.
When I set a break point and then type po dictionary in the console it shows me other things that are set but not this value.
Can anyone see why?
EDIT:
So, After checking the console. It does appear that directly after I save the information to the Keychain it is there. Then I switch views and update another item in the keychain. This then seems to delete the original one and only keep the new item. Both have different names.
Any ideas?
In the exact words of matthew palmer:
Only one piece of data is stored under an account, so when you call updateData it will overwrite whatever's currently stored for that account
so, Everytime you call Locksmith.updateData it basically clears all the data in there and then adds the new value. You need to send both keys and values together.
Try this:
try Locksmith.updateData(["BattleEnd": "12345678", "Key2": "Value Two"], forUserAccount: "gameKeyChainValues")
The dictionary you fetch in the first line isn't a constantly updating view of the keychain, it's just a copy of what's in the keychain when you call that method. It won't be updated when you update the keychain. If you want the up to date values out of the keychain after you update it, call loadDataForUserAccount() again after you update it.