Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to SpeziAccount 0.5.0 with account edit and removal support #8

Merged
merged 13 commits into from
Sep 14, 2023
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ 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", branch: "feature/simple-account-view"),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.13.0")
],
targets: [
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>) {}
}
}
67 changes: 13 additions & 54 deletions Sources/SpeziFirebaseAccount/FirebaseAccountConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,13 @@ import SpeziFirebaseConfiguration
/// }
/// }
/// ```
public final class FirebaseAccountConfiguration: Component, ObservableObject, ObservableObjectProvider {
public final class FirebaseAccountConfiguration: Component {
@Dependency private var configureFirebaseApp: ConfigureFirebaseApp

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 +51,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()
}
}
}
Expand Down
34 changes: 29 additions & 5 deletions Sources/SpeziFirebaseAccount/FirebaseAccountError.swift
Supereg marked this conversation as resolved.
Show resolved Hide resolved
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