Skip to content

Commit

Permalink
Upgrade to SpeziAccount 0.5.0 with account edit and removal support (#8)
Browse files Browse the repository at this point in the history
# Upgrade to upcoming SpeziAccount release

## ♻️ Current situation & Problem
We currently support the current and latest version of `SpeziAccount`
(0.3.0). The next version of `SpeziAccount` heavily refactors the
exposed API.

## 💡 Proposed solution
This PR makes sure SpeziFirebase is ready for the upgrade version of
`SpeziAccount`.

## ⚙️ Release Notes 
* Ensure compatibility with the upcoming release of `SpeziAccount`
* Ability to change user information
* Ability to delete user account
* Improved error message reporting and fixed some spelling mistakes

## ➕ Additional Information

The upgrade to the new version of `SpeziAccount` requires only minor
changes in `SpeziFirebase` itself. We did some refactoring were we moved
all `FIRAuth` operations into the AccountService itself.
Consequentially, the `FirebaseAccountConfiguration` itself got a lot
simpler.

### Breaking Changes

The following breaking changes are induced due to the new structure of
`SpeziAccount`.
* `FirebaseAccountConfiguration` is no longer injected as an environment
object into the SwiftUI environment. Access the account information
using the new, generalized `Account` environment object.

### Related PRs
* StanfordSpezi/SpeziAccount#7

### Testing
Tests were added and adjusted.

### Reviewer Nudging
Look at the changes in `FirebaseAccountConfiguration` and the
`FirebaseEmailPasswordAccountService`.

### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Sep 14, 2023
1 parent d57ddfb commit 0862a0c
Show file tree
Hide file tree
Showing 17 changed files with 1,029 additions and 357 deletions.
3 changes: 0 additions & 3 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,6 @@ only_rules:
# The variable should be placed on the left, the constant on the right of a comparison operator.
- yoda_condition

attributes:
attributes_with_arguments_always_on_line_above: false

deployment_target: # Availability checks or attributes shouldn’t be using older versions that are satisfied by the deployment target.
iOSApplicationExtension_deployment_target: 16.0
iOS_deployment_target: 16.0
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziAccount", .upToNextMinor(from: "0.4.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziAccount", .upToNextMinor(from: "0.5.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.4.2")),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.13.0")
],
targets: [
Expand All @@ -34,6 +35,7 @@ let package = Package(
.target(name: "SpeziFirebaseConfiguration"),
.product(name: "Spezi", package: "Spezi"),
.product(name: "SpeziAccount", package: "SpeziAccount"),
.product(name: "SpeziSecureStorage", package: "SpeziStorage"),
.product(name: "FirebaseAuth", package: "firebase-ios-sdk")
]
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation
import SpeziAccount
import SwiftUI


/// Flag indicating if the firebase account has a verified email address.
///
/// - Important: This key is read-only and cannot be modified.
public struct FirebaseEmailVerifiedKey: AccountKey {
public typealias Value = Bool
public static var name: LocalizedStringResource = "E-Mail Verified"
public static var category: AccountKeyCategory = .other
public static var initialValue: InitialValue<Bool> = .default(false)
}


extension AccountKeys {
/// The email-verified ``FirebaseEmailVerifiedKey`` metatype.
public var isEmailVerified: FirebaseEmailVerifiedKey.Type {
FirebaseEmailVerifiedKey.self
}
}


extension AccountValues {
/// Access if the user's email of their firebase account is verified.
public var isEmailVerified: Bool {
storage[FirebaseEmailVerifiedKey.self] ?? false
}
}


extension FirebaseEmailVerifiedKey {
public struct DataEntry: DataEntryView {
public typealias Key = FirebaseEmailVerifiedKey

public var body: some View {
Text("The FirebaseEmailVerifiedKey cannot be set!")
}

public init(_ value: Binding<Value>) {}
}
}
69 changes: 15 additions & 54 deletions Sources/SpeziFirebaseAccount/FirebaseAccountConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import protocol FirebaseAuth.AuthStateDidChangeListenerHandle
import FirebaseCore
import Foundation
import SpeziFirebaseConfiguration
import SpeziSecureStorage


/// Configures Firebase Auth `AccountService`s that can be used in any views of the `Account` module.
Expand All @@ -34,24 +35,14 @@ import SpeziFirebaseConfiguration
/// }
/// }
/// ```
public final class FirebaseAccountConfiguration: Component, ObservableObject, ObservableObjectProvider {
public final class FirebaseAccountConfiguration: Component {
@Dependency private var configureFirebaseApp: ConfigureFirebaseApp

@Dependency private var secureStorage: SecureStorage

private let emulatorSettings: (host: String, port: Int)?
private let authenticationMethods: FirebaseAuthAuthenticationMethods
private let account: Account
private var authStateDidChangeListenerHandle: AuthStateDidChangeListenerHandle?

@MainActor @Published public var user: User?


public var observableObjects: [any ObservableObject] {
[
self,
account
]
}


@Provide var accountServices: [any AccountService]

/// - Parameters:
/// - emulatorSettings: The emulator settings. The default value is `nil`, connecting the FirebaseAccount module to the Firebase Auth cloud instance.
Expand All @@ -62,54 +53,24 @@ public final class FirebaseAccountConfiguration: Component, ObservableObject, Ob
) {
self.emulatorSettings = emulatorSettings
self.authenticationMethods = authenticationMethods


var accountServices: [any AccountService] = []
self.accountServices = []

if authenticationMethods.contains(.emailAndPassword) {
accountServices.append(FirebaseEmailPasswordAccountService())
self.accountServices.append(FirebaseEmailPasswordAccountService())
}
self.account = Account(accountServices: accountServices)
}


public func configure() {
if let emulatorSettings {
Auth.auth().useEmulator(withHost: emulatorSettings.host, port: emulatorSettings.port)
}

authStateDidChangeListenerHandle = Auth.auth().addStateDidChangeListener { _, user in
guard let user else {
self.updateSignedOut()
return
}

self.updateSignedIn(user)
}

Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { _, error in
guard error == nil else {
self.updateSignedOut()
return
}
}
}

private func updateSignedOut() {
Task {
await MainActor.run {
self.user = nil
self.account.signedIn = false
}
}
}

private func updateSignedIn(_ user: User) {

Task {
await MainActor.run {
self.user = user
if self.account.signedIn == false {
self.account.signedIn = true
}
// We might be configured above the AccountConfiguration and therefore the `Account` object
// might not be injected yet.
try? await Task.sleep(for: .milliseconds(10))
for accountService in accountServices {
await (accountService as? FirebaseEmailPasswordAccountService)?.configure(with: secureStorage)
}
}
}
Expand Down
34 changes: 29 additions & 5 deletions Sources/SpeziFirebaseAccount/FirebaseAccountError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ enum FirebaseAccountError: LocalizedError {
case invalidEmail
case accountAlreadyInUse
case weakPassword
case invalidCredentials
case internalPasswordResetError
case setupError
case notSignedIn
case requireRecentLogin
case unknown(AuthErrorCode.Code)


Expand All @@ -26,8 +30,16 @@ enum FirebaseAccountError: LocalizedError {
return "FIREBASE_ACCOUNT_ALREADY_IN_USE"
case .weakPassword:
return "FIREBASE_ACCOUNT_WEAK_PASSWORD"
case .invalidCredentials:
return "FIREBASE_ACCOUNT_INVALID_CREDENTIALS"
case .internalPasswordResetError:
return "FIREBASE_ACCOUNT_FAILED_PASSWORD_RESET"
case .setupError:
return "FIREBASE_ACCOUNT_SETUP_ERROR"
case .notSignedIn:
return "FIREBASE_ACCOUNT_SIGN_IN_ERROR"
case .requireRecentLogin:
return "FIREBASE_ACCOUNT_REQUIRE_RECENT_LOGIN_ERROR"
case .unknown:
return "FIREBASE_ACCOUNT_UNKNOWN"
}
Expand All @@ -37,10 +49,6 @@ enum FirebaseAccountError: LocalizedError {
.init(localized: errorDescriptionValue, bundle: .module)
}

var failureReason: String? {
errorDescription
}

private var recoverySuggestionValue: String.LocalizationValue {
switch self {
case .invalidEmail:
Expand All @@ -49,8 +57,16 @@ enum FirebaseAccountError: LocalizedError {
return "FIREBASE_ACCOUNT_ALREADY_IN_USE_SUGGESTION"
case .weakPassword:
return "FIREBASE_ACCOUNT_WEAK_PASSWORD_SUGGESTION"
case .invalidCredentials:
return "FIREBASE_ACCOUNT_INVALID_CREDENTIALS_SUGGESTION"
case .internalPasswordResetError:
return "FIREBASE_ACCOUNT_FAILED_PASSWORD_RESET_SUGGESTION"
case .setupError:
return "FIREBASE_ACCOUNT_SETUP_ERROR_SUGGESTION"
case .notSignedIn:
return "FIREBASE_ACCOUNT_SIGN_IN_ERROR_SUGGESTION"
case .requireRecentLogin:
return "FIREBASE_ACCOUNT_REQUIRE_RECENT_LOGIN_ERROR_SUGGESTION"
case .unknown:
return "FIREBASE_ACCOUNT_UNKNOWN_SUGGESTION"
}
Expand All @@ -62,15 +78,23 @@ enum FirebaseAccountError: LocalizedError {


init(authErrorCode: AuthErrorCode) {
FirebaseEmailPasswordAccountService.logger.debug("Received authError with code \(authErrorCode)")

switch authErrorCode.code {
case .invalidEmail:
case .invalidEmail, .invalidRecipientEmail:
self = .invalidEmail
case .emailAlreadyInUse:
self = .accountAlreadyInUse
case .weakPassword:
self = .weakPassword
case .userDisabled, .wrongPassword, .userNotFound, .userMismatch:
self = .invalidCredentials
case .invalidSender, .invalidMessagePayload:
self = .internalPasswordResetError
case .operationNotAllowed, .invalidAPIKey, .appNotAuthorized, .keychainError, .internalError:
self = .setupError
case .requiresRecentLogin:
self = .requireRecentLogin
default:
self = .unknown(authErrorCode.code)
}
Expand Down
Loading

0 comments on commit 0862a0c

Please sign in to comment.