Skip to content

Commit a29ab0a

Browse files
Merge pull request #1237 from firebase/auth-provider-swiftui
2 parents 9831415 + 584774a commit a29ab0a

File tree

51 files changed

+4273
-199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4273
-199
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ DerivedData
1616
*.hmap
1717
*.ipa
1818
*.xcuserstate
19-
19+
.build/
2020
# Third Party
2121
/sdk
2222

.swiftformat

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Formatting Options
2+
--indent 2
3+
--maxwidth 100
4+
--wrapparameters afterfirst
5+
--disable wrapMultilineStatementBraces
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
import SwiftUI
3+
4+
public enum AuthServiceError: LocalizedError {
5+
case invalidEmailLink
6+
case notConfiguredProvider(String)
7+
case clientIdNotFound(String)
8+
case notConfiguredActionCodeSettings
9+
case reauthenticationRequired(String)
10+
case invalidCredentials(String)
11+
case signInFailed(underlying: Error)
12+
13+
public var errorDescription: String? {
14+
switch self {
15+
case .invalidEmailLink:
16+
return "Invalid sign in link. Most likely, the link you used has expired. Try signing in again."
17+
case let .notConfiguredProvider(description):
18+
return description
19+
case let .clientIdNotFound(description):
20+
return description
21+
case .notConfiguredActionCodeSettings:
22+
return "ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`"
23+
case let .reauthenticationRequired(description):
24+
return description
25+
case let .invalidCredentials(description):
26+
return description
27+
case let .signInFailed(underlying: error):
28+
return "Failed to sign in: \(error.localizedDescription)"
29+
}
30+
}
31+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@preconcurrency import FirebaseAuth
2+
import Observation
3+
4+
protocol EmailPasswordOperationReauthentication {
5+
var passwordPrompt: PasswordPromptCoordinator { get }
6+
}
7+
8+
extension EmailPasswordOperationReauthentication {
9+
func reauthenticate() async throws -> AuthenticationToken {
10+
guard let user = Auth.auth().currentUser else {
11+
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
12+
}
13+
14+
guard let email = user.email else {
15+
throw AuthServiceError.invalidCredentials("User does not have an email address")
16+
}
17+
18+
do {
19+
let password = try await passwordPrompt.confirmPassword()
20+
21+
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
22+
try await Auth.auth().currentUser?.reauthenticate(with: credential)
23+
24+
return .firebase("")
25+
} catch {
26+
throw AuthServiceError.signInFailed(underlying: error)
27+
}
28+
}
29+
}
30+
31+
class EmailPasswordDeleteUserOperation: DeleteUserOperation,
32+
EmailPasswordOperationReauthentication {
33+
let passwordPrompt: PasswordPromptCoordinator
34+
35+
init(passwordPrompt: PasswordPromptCoordinator) {
36+
self.passwordPrompt = passwordPrompt
37+
}
38+
}
39+
40+
@MainActor
41+
@Observable
42+
public final class PasswordPromptCoordinator {
43+
var isPromptingPassword = false
44+
private var continuation: CheckedContinuation<String, Error>?
45+
46+
func confirmPassword() async throws -> String {
47+
return try await withCheckedThrowingContinuation { continuation in
48+
self.continuation = continuation
49+
self.isPromptingPassword = true
50+
}
51+
}
52+
53+
func submit(password: String) {
54+
continuation?.resume(returning: password)
55+
cleanup()
56+
}
57+
58+
func cancel() {
59+
continuation?
60+
.resume(throwing: AuthServiceError.reauthenticationRequired("Password entry cancelled"))
61+
cleanup()
62+
}
63+
64+
private func cleanup() {
65+
continuation = nil
66+
isPromptingPassword = false
67+
}
68+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import AuthenticationServices
2+
import FirebaseAuth
3+
4+
extension NSError {
5+
var requiresReauthentication: Bool {
6+
domain == AuthErrorDomain && code == AuthErrorCode.requiresRecentLogin.rawValue
7+
}
8+
9+
var credentialAlreadyInUse: Bool {
10+
domain == AuthErrorDomain && code == AuthErrorCode.credentialAlreadyInUse.rawValue
11+
}
12+
}
13+
14+
enum AuthenticationToken {
15+
case apple(ASAuthorizationAppleIDCredential, String)
16+
case firebase(String)
17+
}
18+
19+
protocol AuthenticatedOperation {
20+
func callAsFunction(on user: User) async throws
21+
func reauthenticate() async throws -> AuthenticationToken
22+
func performOperation(on user: User, with token: AuthenticationToken?) async throws
23+
}
24+
25+
extension AuthenticatedOperation {
26+
func callAsFunction(on user: User) async throws {
27+
do {
28+
try await performOperation(on: user, with: nil)
29+
} catch let error as NSError where error.requiresReauthentication {
30+
let token = try await reauthenticate()
31+
try await performOperation(on: user, with: token)
32+
} catch AuthServiceError.reauthenticationRequired {
33+
let token = try await reauthenticate()
34+
try await performOperation(on: user, with: token)
35+
}
36+
}
37+
}
38+
39+
protocol DeleteUserOperation: AuthenticatedOperation {}
40+
41+
extension DeleteUserOperation {
42+
func performOperation(on user: User, with _: AuthenticationToken? = nil) async throws {
43+
try await user.delete()
44+
}
45+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import FirebaseAuth
2+
import Foundation
3+
4+
public struct AuthConfiguration {
5+
let shouldHideCancelButton: Bool
6+
let interactiveDismissEnabled: Bool
7+
let shouldAutoUpgradeAnonymousUsers: Bool
8+
let customStringsBundle: Bundle?
9+
let tosUrl: URL
10+
let privacyPolicyUrl: URL
11+
let emailLinkSignInActionCodeSettings: ActionCodeSettings?
12+
let verifyEmailActionCodeSettings: ActionCodeSettings?
13+
14+
public init(shouldHideCancelButton: Bool = false,
15+
interactiveDismissEnabled: Bool = true,
16+
shouldAutoUpgradeAnonymousUsers: Bool = false,
17+
customStringsBundle: Bundle? = nil,
18+
tosUrl: URL = URL(string: "https://example.com/tos")!,
19+
privacyPolicyUrl: URL = URL(string: "https://example.com/privacy")!,
20+
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
21+
verifyEmailActionCodeSettings: ActionCodeSettings? = nil) {
22+
self.shouldHideCancelButton = shouldHideCancelButton
23+
self.interactiveDismissEnabled = interactiveDismissEnabled
24+
self.shouldAutoUpgradeAnonymousUsers = shouldAutoUpgradeAnonymousUsers
25+
self.customStringsBundle = customStringsBundle
26+
self.tosUrl = tosUrl
27+
self.privacyPolicyUrl = privacyPolicyUrl
28+
self.emailLinkSignInActionCodeSettings = emailLinkSignInActionCodeSettings
29+
self.verifyEmailActionCodeSettings = verifyEmailActionCodeSettings
30+
}
31+
}

0 commit comments

Comments
 (0)