diff --git a/Package.swift b/Package.swift index 3cd4d15..a25ea01 100644 --- a/Package.swift +++ b/Package.swift @@ -25,10 +25,10 @@ let package = Package( .library(name: "SpeziFirebaseAccountStorage", targets: ["SpeziFirebaseAccountStorage"]) ], dependencies: [ - .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")), - .package(url: "https://github.com/StanfordSpezi/SpeziViews.git", .upToNextMinor(from: "0.6.1")), - .package(url: "https://github.com/StanfordSpezi/SpeziAccount", .upToNextMinor(from: "0.8.1")), - .package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.5.0")), + .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.0.0"), + .package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.0.0"), + .package(url: "https://github.com/StanfordSpezi/SpeziStorage", from: "1.0.0"), + .package(url: "https://github.com/StanfordSpezi/SpeziAccount", from: "1.0.0"), .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.13.0") ], targets: [ diff --git a/README.md b/README.md index 547ef20..24f483c 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,87 @@ SPDX-License-Identifier: MIT [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziFirebase%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziFirebase) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziFirebase%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziFirebase) -Google Firebase modules for the [Spezi framework](https://github.com/StanfordSpezi/Spezi). +Integrate Google Firebase services into your Spezi application. + +## Overview + +This Module allows you to use the [Google Firebase](https://firebase.google.com/) platform as a managed backend for +authentication and data storage in your apps built with the [Spezi framework](https://github.com/StanfordSpezi/Spezi). + +We currently implement support for Authentication, Storage, and Firestore services. + +## Setup + +You need to add the Spezi Firebase Swift package to +[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or +[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). + +> [!IMPORTANT] +> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup)# + to set up the core Spezi infrastructure. + + +## Examples + +The below section walks you through the necessary steps to set up the Spezi Firebase Module for your application. + +### 1. Set up your Firebase Account + +To connect your app to the Firebase cloud platform, you will need to first create an account at +[firebase.google.com](https://firebase.google.com) then start the process to +[register a new iOS app](https://firebase.google.com/docs/ios/setup). + +Once your Spezi app is registered with Firebase, place the generated `GoogleService-Info.plist` configuration file +into the root of your Xcode project. +You do not need to add the Firebase SDKs to your app or initialize Firebase in your app, +since the Spezi Firebase Module will handle these tasks for you. + +You can also install and run the Firebase Local Emulator Suite for local development. +To do this, please follow the [installation instructions](https://firebase.google.com/docs/emulator-suite/install_and_configure). + +### 2. Add Spezi Firebase as a Dependency + +First, you will need to add the SpeziFirebase Swift package to +[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or +[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). + +### 3. Register the Spezi Firebase Modules + +In the example below, we configure our Spezi application to use Firebase Authentication with both email & password login +and Sign in With Apple, and Cloud Firestore for data storage. + +```swift +import Spezi +import SpeziAccount +import SpeziFirebaseAccount +import SpeziFirebaseStorage +import SpeziFirestore +import SwiftUI + + +class ExampleDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + AccountConfiguration(configuration: [ + .requires(\.userId), + .collects(\.name) + ]) + Firestore() + FirebaseAccountConfiguration[ + authenticationMethods: [.emailAndPassword, .signInWithApple] + ] + } + } +} +``` For more information, please refer to the [API documentation](https://swiftpackageindex.com/StanfordSpezi/SpeziFirebase/documentation). ## The Spezi Template Application -The [Spezi Template Application](https://github.com/StanfordSpezi/SpeziTemplateApplication) provides a great starting point and example using the Spezi Firebase Modules. +The Spezi Firebase Module comes pre-configured in the [Spezi Template Application](https://github.com/StanfordSpezi/SpeziTemplateApplication), +which is a great way to get started on your Spezi Application. ## Contributing diff --git a/Sources/SpeziFirebaseAccount/FirebaseAccountConfiguration.swift b/Sources/SpeziFirebaseAccount/FirebaseAccountConfiguration.swift index f3dcad3..1661daf 100644 --- a/Sources/SpeziFirebaseAccount/FirebaseAccountConfiguration.swift +++ b/Sources/SpeziFirebaseAccount/FirebaseAccountConfiguration.swift @@ -18,18 +18,13 @@ import SpeziLocalStorage import SpeziSecureStorage -/// Configures Firebase Auth `AccountService`s that can be used in any views of the `Account` module. +/// Configures an `AccountService` to interact with Firebase Auth. /// -/// The ``FirebaseAccountConfiguration`` offers a ``user`` property to access the current Firebase Auth user from, e.g., a SwiftUI view's environment: -/// ``` -/// @EnvironmentObject var firebaseAccountConfiguration: FirebaseAccountConfiguration -/// ``` -/// -/// The ``FirebaseAccountConfiguration`` can, e.g., be used to to connect to the Firebase Auth emulator: +/// The `FirebaseAccountConfiguration` can, e.g., be used to to connect to the Firebase Auth emulator: /// ``` /// class ExampleAppDelegate: SpeziAppDelegate { /// override var configuration: Configuration { -/// Configuration(standard: /* ... */) { +/// Configuration { /// FirebaseAccountConfiguration(emulatorSettings: (host: "localhost", port: 9099)) /// // ... /// } diff --git a/Sources/SpeziFirebaseAccount/Models/FirebaseContext.swift b/Sources/SpeziFirebaseAccount/Models/FirebaseContext.swift index 82d443e..012af13 100644 --- a/Sources/SpeziFirebaseAccount/Models/FirebaseContext.swift +++ b/Sources/SpeziFirebaseAccount/Models/FirebaseContext.swift @@ -65,7 +65,13 @@ actor FirebaseContext { // if there is a cached user, we refresh the authentication token Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { _, error in - if error != nil { + if let error { + let code = AuthErrorCode(_nsError: error as NSError) + + guard code.code != .networkError else { + return // we make sure that we don't remove the account when we don't have network (e.g., flight mode) + } + Task { try await self.notifyUserRemoval(for: self.lastActiveAccountService) } diff --git a/Sources/SpeziFirebaseAccount/SpeziFirebaseAccount.docc/SpeziFirebaseAccount.md b/Sources/SpeziFirebaseAccount/SpeziFirebaseAccount.docc/SpeziFirebaseAccount.md new file mode 100644 index 0000000..addb392 --- /dev/null +++ b/Sources/SpeziFirebaseAccount/SpeziFirebaseAccount.docc/SpeziFirebaseAccount.md @@ -0,0 +1,44 @@ +# ``SpeziFirebaseAccount`` + + + +Firebase Auth support for SpeziAccount. + +## Overview + +This Module adds support for Firebase Auth for SpeziAccount by implementing a respective + [AccountService](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountservice). + +The `FirebaseAccountConfiguration` can, e.g., be used to to connect to the Firebase Auth emulator: +``` +class ExampleAppDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + FirebaseAccountConfiguration(emulatorSettings: (host: "localhost", port: 9099)) + // ... + } + } +} +``` + +## Topics + +### Firebase Account + +- ``FirebaseAccountConfiguration`` +- ``FirebaseAuthAuthenticationMethods`` + +### Account Keys + +- ``FirebaseEmailVerifiedKey`` +- ``SpeziAccount/AccountValues/isEmailVerified`` +- ``SpeziAccount/AccountKeys/isEmailVerified`` + diff --git a/Sources/SpeziFirebaseAccountStorage/FirestoreAccountStorage.swift b/Sources/SpeziFirebaseAccountStorage/FirestoreAccountStorage.swift index 1352c0c..0c3a428 100644 --- a/Sources/SpeziFirebaseAccountStorage/FirestoreAccountStorage.swift +++ b/Sources/SpeziFirebaseAccountStorage/FirestoreAccountStorage.swift @@ -12,7 +12,7 @@ import SpeziAccount import SpeziFirestore -/// Store additional account details directly in firestore. +/// Store additional account details directly in Firestore. /// /// Certain account services, like the account services provided by Firebase, can only store certain account details. /// The `FirestoreAccountStorage` can be used to store additional account details, that are not supported out of the box by your account services, @@ -23,7 +23,7 @@ import SpeziFirestore /// /// Once you have [AccountConfiguration](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/initial-setup#Account-Configuration) /// and the [FirebaseAccountConfiguration](https://swiftpackageindex.com/stanfordspezi/spezifirebase/documentation/spezifirebaseaccount/firebaseaccountconfiguration) -/// set up, you can adopt the [AccountStorageStandard](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountstoragestandard) +/// set up, you can adopt the [AccountStorageConstraint](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountstorageconstraint) /// protocol to provide a custom storage for SpeziAccount. /// /// - Important: In order to use the `FirestoreAccountStorage`, you must have [Firestore](https://swiftpackageindex.com/stanfordspezi/spezifirebase/main/documentation/spezifirestore/firestore) @@ -36,7 +36,7 @@ import SpeziFirestore /// import SpeziFirebaseAccountStorage /// /// -/// actor ExampleStandard: Standard, AccountStorageStandard { +/// actor ExampleStandard: Standard, AccountStorageConstraint { /// // Define the collection where you want to store your additional user data, ... /// static var collection: CollectionReference { /// Firestore.firestore().collection("users") @@ -46,7 +46,7 @@ import SpeziFirestore /// @Dependency private var accountStorage = FirestoreAccountStorage(storedIn: Self.collection) /// /// -/// // ... and forward all implementations of `AccountStorageStandard` to the `FirestoreAccountStorage`. +/// // ... and forward all implementations of `AccountStorageConstraint` to the `FirestoreAccountStorage`. /// /// public func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { /// try await accountStorage.create(identifier, details) @@ -69,7 +69,7 @@ import SpeziFirestore /// } /// } /// ``` -public actor FirestoreAccountStorage: Module, AccountStorageStandard { +public actor FirestoreAccountStorage: Module, AccountStorageConstraint { @Dependency private var firestore: SpeziFirestore.Firestore // ensure firestore is configured private let collection: () -> CollectionReference diff --git a/Sources/SpeziFirebaseAccountStorage/SpeziFirebaseAccountStorage.docc/SpeziFirebaseAccountStorage.md b/Sources/SpeziFirebaseAccountStorage/SpeziFirebaseAccountStorage.docc/SpeziFirebaseAccountStorage.md new file mode 100644 index 0000000..1b27d50 --- /dev/null +++ b/Sources/SpeziFirebaseAccountStorage/SpeziFirebaseAccountStorage.docc/SpeziFirebaseAccountStorage.md @@ -0,0 +1,76 @@ +# ``SpeziFirebaseAccountStorage`` + + + +Store additional account details directly in Firestore. + +## Overview + +Certain account services, like the account services provided by Firebase, can only store certain account details. +The ``FirestoreAccountStorage`` can be used to store additional account details, that are not supported out of the box by your account services, +inside Firestore in a custom user collection. + +For more detailed information, refer to the documentation of ``FirestoreAccountStorage``. + +### Example + +Once you have [AccountConfiguration](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/initial-setup#Account-Configuration) +and the [FirebaseAccountConfiguration](https://swiftpackageindex.com/stanfordspezi/spezifirebase/documentation/spezifirebaseaccount/firebaseaccountconfiguration) +set up, you can adopt the [AccountStorageConstraint](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountstorageconstraint) +protocol to provide a custom storage for SpeziAccount. + + +```swift +import FirebaseFirestore +import Spezi +import SpeziAccount +import SpeziFirebaseAccountStorage + + +actor ExampleStandard: Standard, AccountStorageConstraint { + // Define the collection where you want to store your additional user data, ... + static var collection: CollectionReference { + Firestore.firestore().collection("users") + } + + // ... define and initialize the `FirestoreAccountStorage` dependency ... + @Dependency private var accountStorage = FirestoreAccountStorage(storedIn: Self.collection) + + + // ... and forward all implementations of `AccountStorageConstraint` to the `FirestoreAccountStorage`. + + public func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { + try await accountStorage.create(identifier, details) + } + + public func load(_ identifier: AdditionalRecordId, _ keys: [any AccountKey.Type]) async throws -> PartialAccountDetails { + try await accountStorage.load(identifier, keys) + } + + public func modify(_ identifier: AdditionalRecordId, _ modifications: AccountModifications) async throws { + try await accountStorage.modify(identifier, modifications) + } + + public func clear(_ identifier: AdditionalRecordId) async { + await accountStorage.clear(identifier) + } + + public func delete(_ identifier: AdditionalRecordId) async throws { + try await accountStorage.delete(identifier) + } +} +``` + +## Topics + +### Storage + +- ``FirestoreAccountStorage`` diff --git a/Sources/SpeziFirebaseConfiguration/ConfigureFirebaseApp.swift b/Sources/SpeziFirebaseConfiguration/ConfigureFirebaseApp.swift index 7007fe6..e562c2b 100644 --- a/Sources/SpeziFirebaseConfiguration/ConfigureFirebaseApp.swift +++ b/Sources/SpeziFirebaseConfiguration/ConfigureFirebaseApp.swift @@ -10,13 +10,13 @@ import FirebaseCore import Spezi -/// Shared component to serve as a single point to configure the Firebase set of dependencies. +/// Module to configure the Firebase set of dependencies. /// /// The ``configure()`` method calls `FirebaseApp.configure()`. -/// Use the `@Dependency` property wrapper to define a dependency on this component and ensure that `FirebaseApp.configure()` is called before any -/// other Firebase-related components: +/// Use the `@Dependency` property wrapper to define a dependency on this module and ensure that `FirebaseApp.configure()` is called before any +/// other Firebase-related modules: /// ```swift -/// public final class YourFirebaseComponent: Component { +/// public final class YourFirebaseModule: Module { /// @Dependency private var configureFirebaseApp: ConfigureFirebaseApp /// /// // ... diff --git a/Sources/SpeziFirebaseConfiguration/FirebaseConfiguration.docc/FirebaseConfiguration.md b/Sources/SpeziFirebaseConfiguration/FirebaseConfiguration.docc/FirebaseConfiguration.md index 05323f2..408c243 100644 --- a/Sources/SpeziFirebaseConfiguration/FirebaseConfiguration.docc/FirebaseConfiguration.md +++ b/Sources/SpeziFirebaseConfiguration/FirebaseConfiguration.docc/FirebaseConfiguration.md @@ -1,4 +1,4 @@ -# ``FirebaseConfiguration`` +# ``SpeziFirebaseConfiguration`` -Shared component to serve as a single point to configure the Firebase set of dependencies. +Module to configure the Firebase set of dependencies. ## Overview -The `configure()` method calls `FirebaseApp.configure()`. +The ``ConfigureFirebaseApp/configure()`` method calls `FirebaseApp.configure()`. -Use the `@Dependency` property wrapper to define a dependency on this component and ensure that `FirebaseApp.configure()` is called before any -other Firebase-related components: +Use the `@Dependency` property wrapper to define a dependency on this module and ensure that `FirebaseApp.configure()` is called before any +other Firebase-related modules: ```swift -public final class YourFirebaseComponent: Component { +public final class YourFirebaseModule: Module { @Dependency private var configureFirebaseApp: ConfigureFirebaseApp // ... diff --git a/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift b/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift index 0ed6bd8..7f82905 100644 --- a/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift +++ b/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift @@ -17,7 +17,7 @@ import SpeziFirebaseConfiguration /// ``` /// class ExampleAppDelegate: SpeziAppDelegate { /// override var configuration: Configuration { -/// Configuration(standard: /* ... */) { +/// Configuration { /// FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) /// // ... /// } diff --git a/Sources/SpeziFirebaseStorage/SpeziFirebaseStorage.docc/SpeziFirebaseStorage.md b/Sources/SpeziFirebaseStorage/SpeziFirebaseStorage.docc/SpeziFirebaseStorage.md new file mode 100644 index 0000000..169c883 --- /dev/null +++ b/Sources/SpeziFirebaseStorage/SpeziFirebaseStorage.docc/SpeziFirebaseStorage.md @@ -0,0 +1,35 @@ +# ``SpeziFirebaseStorage`` + + + +Firebase Storage related components. + +## Overview + +Configures the Firebase Storage that can then be used within any application via `Storage.storage()`. + +The ``FirebaseStorageConfiguration`` can be used to connect to the Firebase Storage emulator: +``` +class ExampleAppDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) + // ... + } + } +} +``` + +## Topics + +### Firebase Storage + +- ``FirebaseStorageConfiguration`` diff --git a/Sources/SpeziFirestore/Firestore.swift b/Sources/SpeziFirestore/Firestore.swift index 010ab57..4f0fca9 100644 --- a/Sources/SpeziFirestore/Firestore.swift +++ b/Sources/SpeziFirestore/Firestore.swift @@ -14,7 +14,7 @@ import SpeziFirebaseConfiguration import SwiftUI -/// The ``Firestore`` module & data storage provider enables the synchronization of data stored in a standard with the Firebase Firestore. +/// The ``Firestore`` module allows for easy configuration of Firebase Firestore. /// /// You can configure the ``Firestore`` module in the `SpeziAppDelegate`, e.g. the configure it using the Firebase emulator. /// ```swift @@ -28,8 +28,8 @@ import SwiftUI /// } /// ``` /// -/// We recommend using the [FIrebase Fireastore SDK as defined in the API documentation](https://firebase.google.com/docs/firestore/manage-data/add-data#swift) -/// throughout the application. We **highly recommend using the async/await variants of the APIs** instead of the closure-based APIs the SDK provides. +/// - Note: We recommend using the [Firebase Firestore SDK as defined in the API documentation](https://firebase.google.com/docs/firestore/manage-data/add-data#swift) +/// throughout the application. We **highly recommend using the async/await variants of the APIs** instead of the closure-based APIs the SDK provides. public class Firestore: Module, DefaultInitializable { @Dependency private var configureFirebaseApp: ConfigureFirebaseApp diff --git a/Sources/SpeziFirestore/FirestoreErrorCode.swift b/Sources/SpeziFirestore/FirestoreErrorCode.swift index d381be4..12ed60c 100644 --- a/Sources/SpeziFirestore/FirestoreErrorCode.swift +++ b/Sources/SpeziFirestore/FirestoreErrorCode.swift @@ -11,7 +11,7 @@ import FirebaseFirestoreSwift import Foundation -// swiftlint:disable cyclomatic_complexity function_body_length +/// Mapping of Firestore error codes to a localized error. public enum FirestoreError: LocalizedError { case cancelled case invalidArgument @@ -82,7 +82,7 @@ public enum FirestoreError: LocalizedError { } - public init(_ error: E) { + public init(_ error: E) { // swiftlint:disable:this cyclomatic_complexity function_body_length if let firestoreError = error as? Self { self = firestoreError return diff --git a/Sources/SpeziFirestore/SpeziFirestore.docc/SpeziFirestore.md b/Sources/SpeziFirestore/SpeziFirestore.docc/SpeziFirestore.md new file mode 100644 index 0000000..358b388 --- /dev/null +++ b/Sources/SpeziFirestore/SpeziFirestore.docc/SpeziFirestore.md @@ -0,0 +1,36 @@ +# ``SpeziFirestore`` + + + +Easily configure and interact with Firebase Firestore. + +## Overview + +The ``Firestore`` module allows for easy configuration of Firebase Firestore. + +You can configure the ``Firestore`` module in the `SpeziAppDelegate`, e.g. the configure it using the Firebase emulator. +```swift +class FirestoreExampleDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + Firestore(settings: .emulator) + // ... + } + } +} +``` + +## Topics + +### Firestore + +- ``Firestore`` +- ``FirestoreError`` diff --git a/Tests/UITests/TestApp/FirebaseAccountStorage/AccountStorageTestStandard.swift b/Tests/UITests/TestApp/FirebaseAccountStorage/AccountStorageTestStandard.swift index c072798..5c026b6 100644 --- a/Tests/UITests/TestApp/FirebaseAccountStorage/AccountStorageTestStandard.swift +++ b/Tests/UITests/TestApp/FirebaseAccountStorage/AccountStorageTestStandard.swift @@ -12,7 +12,7 @@ import SpeziAccount import SpeziFirebaseAccountStorage -actor AccountStorageTestStandard: Standard, AccountStorageStandard { +actor AccountStorageTestStandard: Standard, AccountStorageConstraint { static var collection: CollectionReference { Firestore.firestore().collection("users") }