firebase / firebase-ios-sdk

Firebase SDK for Apple App Development
https://firebase.google.com
Apache License 2.0
5.59k stars 1.46k forks source link

Could not reach Cloud Firestore backend. Backend didn't respond within 10 seconds #13235

Closed AlexWaldron1 closed 2 months ago

AlexWaldron1 commented 3 months ago

Description

When I launch the app I get stuck on the launch screen and I eventually get the error: Could not reach Cloud Firestore backend. Backend didn't respond within 10 seconds. This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.

I have checked on multiple different devices on both LTE and wifi over the course of a few days and I have no issues with connectivity. So I'm wondering if I set something else up incorrectly?

//
//  FirebaseClient.swift
//  languagelearningapp
//
//  Created by Alex Waldron on 6/3/24.
//

import OpenAI
import Combine
import Dispatch
import FirebaseAppCheck
import FirebaseFirestore
import FirebaseFirestoreSwift
import FirebaseFunctions

final class FirebaseClient {
    enum FirestoreCollection: String {
        case user = "language-app"
        case conversations
        case messages
        case flashcards
    }
    public static var level: Level = .beginner

    private static var functions = Functions.functions()

    private static var db: Firestore = .firestore()
    private static var userId = UserDefaults.standard.string(forKey: "userId")
    private static var conversationPublisher = PassthroughSubject<[ConversationTitle], Error>()
    public static var conversationUpdates: AnyPublisher<[ConversationTitle], Error> {
        return conversationPublisher.eraseToAnyPublisher()
    }
    private static var flashcardPublisher = PassthroughSubject<[Flashcard], Error>()
    public static var flashcardUpdates: AnyPublisher<[Flashcard], Error> {
        return flashcardPublisher.eraseToAnyPublisher()
    }

    init() {
        #if DEBUG
        Firestore.enableLogging(true)
        let settings = Firestore.firestore().settings
        settings.host = "localhost:8080"
        settings.isSSLEnabled = false
        FirebaseClient.db.settings = settings
        #endif
    }

    private static func getCollection(_ collection: FirestoreCollection, parent: (any FirestoreDocument)? = nil) -> CollectionReference {
        let path = (parent?.documentPath ?? "") + "/" + collection.rawValue

        return db.collection(path)
    }

    private static func convertDocuments<T: FirestoreDocument>(_ documents: [QueryDocumentSnapshot], toArrayOf type: T.Type) throws -> [T] {
        var result = [T]()
        for doc in documents {
            let obj = try doc.dataWithDefaultValues(as: T.self)
            result.append(obj)
        }
        return result
    }
    public static func getOpenAIResponse(content: String, completion: @escaping (Result<String, Error>) -> Void) {
        let query: [NSDictionary] = [APIMessage(role: "user", content: content).nsDictionary, APIMessage(role: "system", content: "reply using \(level) level vocabulary please.").nsDictionary]

        functions.httpsCallable("getOpenAIResponse").call(query) { result, error in
            if let error = error {
                completion(.failure(APIError.networkError(error)))
            }
            if let response = result?.data as? [String: Any], let content = response["response"] as? String {
                completion(.success(content))
            } else {
                print("casting failed")
                completion(.failure(APIError.parsingError))
            }
        }
    }

    public static func getOpenAIResponse(content: String, system: String, completion: @escaping (Result<String, Error>) -> Void) {
        let query: [NSDictionary] = [APIMessage(role: "user", content: content).nsDictionary, APIMessage(role: "system", content: system).nsDictionary]

        functions.httpsCallable("getOpenAIResponse").call(query) { result, error in
            if let error = error {
                completion(.failure(APIError.networkError(error)))
            }
            if let response = result?.data as? [String: Any], let content = response["response"] as? String {
                completion(.success(content))
            } else {
                print("casting failed")
                completion(.failure(APIError.parsingError))
            }
        }
    }
    public static func fetchUser() async throws -> [User] {
        guard let id = userId else { return  [] }
        let snapshot = try await getCollection(.user).whereField("id", isEqualTo: id).getDocuments()

        return try convertDocuments(snapshot.documents, toArrayOf: User.self)
    }

    public static func createUser() async throws {
        guard let id = userId else { return }
        try await getCollection(.user).document(id).setData(["id": id])
    }
    public static func fetchConversations(user: User) async throws -> [ConversationTitle] {
        let snapshot = try await getCollection(.conversations, parent: user).getDocuments()
        return try convertDocuments(snapshot.documents, toArrayOf: ConversationTitle.self)
    }
    public static func fetchMessages(title: ConversationTitle) async throws -> [Message] {
        let snapshot = try await getCollection(.messages, parent: title).getDocuments()
        return try convertDocuments(snapshot.documents, toArrayOf: Message.self)
    }
    public static func listenForFlashcardChanges(user: User) throws {
        getCollection(.flashcards, parent: user).addSnapshotListener { querySnapshot, error in
            guard let documents = querySnapshot?.documents else {
              print("Error fetching documents: \(error!)")
              return
            }
            do {
                let flashcards = try convertDocuments(documents, toArrayOf: Flashcard.self)
                self.flashcardPublisher.send(flashcards)
            } catch let error {
                print("can't make it happen bubba \(error)")
            }
          }
    }

    public static func listenForTitleChanges(user: User) throws {
        getCollection(.conversations, parent: user).addSnapshotListener { querySnapshot, error in
            guard let documents = querySnapshot?.documents else {
              print("Error fetching documents: \(error!)")
              return
            }
            do {
                let titles = try convertDocuments(documents, toArrayOf: ConversationTitle.self)
                self.conversationPublisher.send(titles)
            } catch let error {
                print("can't make it happen bubba \(error)")
            }
          }
    }

    public static func deleteConversation(id: String) async throws {
        do {
            guard let user = try await fetchUser().first else { return }
            try await getCollection(.conversations, parent: user).document(id).delete()
          print("Document successfully removed!")
        } catch {
          print("Error removing document: \(error)")
        }
    }

    public static func fetchConversationsByDate(date : Date) async throws -> [ConversationTitle] {
        guard let user = try await fetchUser().first else { return [] }
        let snapshot = try await getCollection(.conversations, parent: user).whereField("date", isFromToday: date).getDocuments()
        return try convertDocuments(snapshot.documents, toArrayOf: ConversationTitle.self)
    }

    public static func fetchMessages(from conversations: [ConversationTitle]) async throws ->  [Message] {
        var messages = [Message]()
        try await conversations.asyncForEach { conversation in
            let words = try await fetchMessages(title: conversation)
            messages.append(contentsOf: words)
        }
        return messages
    }

    public static func save(conversation: [MessageToSave], title: String) {
        guard let id = userId else {
            return
        }
        db.collection("language-app").document(id).setData(["id": id])
        db.collection("language-app").document(id).collection("conversations").document(title).setData(["title": title, "date": Date.now])
        do {
            try conversation.indices.forEach { index in
                try db.collection("language-app").document(id)
                    .collection("conversations").document(title)
                    .collection("messages").document("\(index)")
                    .setData(from: conversation[index])
            }
        } catch let error {
          print("Error writing to Firestore: \(error)")
        }
    }

    public static func save(english: String, foreign: String) {
        guard let id = userId else {
            return
        }
        db.collection("language-app").document(id).setData(["id": id])
        db.collection("language-app").document(id).collection("flashcards").document().setData(["english": english, "foreign": foreign])
    }
}

private extension FirebaseClient {
    struct APIMessage: Codable {
        let role: String
        let content: String
        var dictionary: [String: Any] {
            return ["role": role,
                    "content": content]
        }
        var nsDictionary: NSDictionary {
            return dictionary as NSDictionary
        }
    }
}

public extension DocumentSnapshot {
  func dataWithDefaultValues<T: Decodable>(as type: T.Type,
                          with serverTimestampBehavior: ServerTimestampBehavior = .none,
                          decoder: Firestore.Decoder = .init()) throws -> T {
    let d: Any = data(with: serverTimestampBehavior) ?? NSNull()
      if var dict = d as? [String: Any] {
          dict["documentId"] = self.documentID
          dict["documentPath"] = self.reference.path
          return try decoder.decode(T.self, from: dict, in: reference)
      }
    return try decoder.decode(T.self, from: d, in: reference)
  }
}

extension CollectionReference {
    func whereField(_ field: String, isFromToday value: Date) -> Query {
        let calendar = Calendar.current
        guard let oneDayAgo = calendar.date(byAdding: .day, value: -1, to: value) else {
            fatalError("Unable to calculate the date one week ago.")
        }
        return whereField(field, isGreaterThanOrEqualTo: oneDayAgo).whereField(field, isLessThanOrEqualTo: value)
    }
}

Reproducing the issue

Launch the app, wait for error to occur, get stuck on launch screen

Firebase SDK Version

10.27.0

Xcode Version

15.4

Installation Method

Swift Package Manager

Firebase Product(s)

App Check, Firestore, Functions

Targeted Platforms

iOS

Relevant Log Output

https://gist.github.com/AlexWaldron1/0d7da9295637655ac8c3ed9ae2a85faf

If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet
```json Replace this line with the contents of your Package.resolved. ```

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
```yml Replace this line with the contents of your Podfile.lock! ```
wu-hui commented 3 months ago

The code snippet looks fine. Is this a standalone iOS App or it is run from some restricted environments like extensions?

AlexWaldron1 commented 2 months ago

Hey @wu-hui I'm sorry I just had a weird ui issue that was causing the app to freeze while trying to reach firebase. I fixed it and now it's all working. Sorry to have wasted any time!

AlexWaldron1 commented 2 months ago

Resolved on my own!