Skip to content

Commit cdaa05f

Browse files
authored
[Auth] Convert *Response classes to structs (#14012)
1 parent 15eb852 commit cdaa05f

Some content is hidden

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

41 files changed

+363
-401
lines changed

FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

Lines changed: 60 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -22,82 +22,61 @@ import Foundation
2222
#endif
2323

2424
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
25-
protocol AuthBackendRPCIssuer {
26-
/// Asynchronously send a HTTP request.
27-
/// - Parameter request: The request to be made.
28-
/// - Parameter body: Request body.
29-
/// - Parameter contentType: Content type of the body.
30-
/// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously
31-
/// on the auth global work queue in the future.
32-
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
33-
body: Data?,
34-
contentType: String) async -> (Data?, Error?)
35-
}
36-
37-
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
38-
class AuthBackendRPCIssuerImplementation: AuthBackendRPCIssuer {
39-
let fetcherService: GTMSessionFetcherService
40-
41-
init() {
42-
fetcherService = GTMSessionFetcherService()
43-
fetcherService.userAgent = AuthBackend.authUserAgent()
44-
fetcherService.callbackQueue = kAuthGlobalWorkQueue
45-
46-
// Avoid reusing the session to prevent
47-
// https://github.com/firebase/firebase-ios-sdk/issues/1261
48-
fetcherService.reuseSession = false
49-
}
50-
51-
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
52-
body: Data?,
53-
contentType: String) async -> (Data?, Error?) {
54-
let requestConfiguration = request.requestConfiguration()
55-
let request = await AuthBackend.request(withURL: request.requestURL(),
56-
contentType: contentType,
57-
requestConfiguration: requestConfiguration)
58-
let fetcher = fetcherService.fetcher(with: request)
59-
if let _ = requestConfiguration.emulatorHostAndPort {
60-
fetcher.allowLocalhostRequest = true
61-
fetcher.allowedInsecureSchemes = ["http"]
62-
}
63-
fetcher.bodyData = body
64-
65-
return await withUnsafeContinuation { continuation in
66-
fetcher.beginFetch { data, error in
67-
continuation.resume(returning: (data, error))
68-
}
69-
}
70-
}
25+
protocol AuthBackendProtocol {
26+
func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response
7127
}
7228

7329
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
74-
class AuthBackend {
30+
class AuthBackend: AuthBackendProtocol {
7531
static func authUserAgent() -> String {
7632
return "FirebaseAuth.iOS/\(FirebaseVersion()) \(GTMFetcherStandardUserAgentString(nil))"
7733
}
7834

79-
private static var realRPCBackend = AuthBackendRPCImplementation()
80-
private static var gBackendImplementation = realRPCBackend
35+
static func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
36+
return try await shared.call(with: request)
37+
}
8138

82-
class func setTestRPCIssuer(issuer: AuthBackendRPCIssuer) {
83-
gBackendImplementation.rpcIssuer = issuer
39+
static func setTestRPCIssuer(issuer: AuthBackendRPCIssuer) {
40+
shared.rpcIssuer = issuer
8441
}
8542

86-
class func resetRPCIssuer() {
87-
gBackendImplementation.rpcIssuer = realRPCBackend.rpcIssuer
43+
static func resetRPCIssuer() {
44+
shared.rpcIssuer = AuthBackendRPCIssuer()
8845
}
8946

90-
class func implementation() -> AuthBackendImplementation {
91-
return gBackendImplementation
47+
private static let shared: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer())
48+
49+
private var rpcIssuer: any AuthBackendRPCIssuerProtocol
50+
51+
init(rpcIssuer: any AuthBackendRPCIssuerProtocol) {
52+
self.rpcIssuer = rpcIssuer
9253
}
9354

94-
class func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
95-
return try await implementation().call(with: request)
55+
/// Calls the RPC using HTTP request.
56+
/// Possible error responses:
57+
/// * See FIRAuthInternalErrorCodeRPCRequestEncodingError
58+
/// * See FIRAuthInternalErrorCodeJSONSerializationError
59+
/// * See FIRAuthInternalErrorCodeNetworkError
60+
/// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse
61+
/// * See FIRAuthInternalErrorCodeUnexpectedResponse
62+
/// * See FIRAuthInternalErrorCodeRPCResponseDecodingError
63+
/// - Parameter request: The request.
64+
/// - Returns: The response.
65+
func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
66+
let response = try await callInternal(with: request)
67+
if let auth = request.requestConfiguration().auth,
68+
let mfaError = Self.generateMFAError(response: response, auth: auth) {
69+
throw mfaError
70+
} else if let error = Self.phoneCredentialInUse(response: response) {
71+
throw error
72+
} else {
73+
return response
74+
}
9675
}
9776

98-
class func request(withURL url: URL,
99-
contentType: String,
100-
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
77+
static func request(withURL url: URL,
78+
contentType: String,
79+
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
10180
// Kick off tasks for the async header values.
10281
async let heartbeatsHeaderValue = requestConfiguration.heartbeatLogger?.asyncHeaderValue()
10382
async let appCheckTokenHeaderValue = requestConfiguration.appCheck?
@@ -132,41 +111,11 @@ class AuthBackend {
132111
}
133112
return request
134113
}
135-
}
136114

137-
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
138-
protocol AuthBackendImplementation {
139-
func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response
140-
}
141-
142-
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
143-
private class AuthBackendRPCImplementation: AuthBackendImplementation {
144-
var rpcIssuer: AuthBackendRPCIssuer = AuthBackendRPCIssuerImplementation()
145-
146-
/// Calls the RPC using HTTP request.
147-
/// Possible error responses:
148-
/// * See FIRAuthInternalErrorCodeRPCRequestEncodingError
149-
/// * See FIRAuthInternalErrorCodeJSONSerializationError
150-
/// * See FIRAuthInternalErrorCodeNetworkError
151-
/// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse
152-
/// * See FIRAuthInternalErrorCodeUnexpectedResponse
153-
/// * See FIRAuthInternalErrorCodeRPCResponseDecodingError
154-
/// - Parameter request: The request.
155-
/// - Returns: The response.
156-
fileprivate func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
157-
let response = try await callInternal(with: request)
158-
if let auth = request.requestConfiguration().auth,
159-
let mfaError = Self.generateMFAError(response: response, auth: auth) {
160-
throw mfaError
161-
} else if let error = Self.phoneCredentialInUse(response: response) {
162-
throw error
163-
} else {
164-
return response
165-
}
166-
}
167-
168-
#if os(iOS)
169-
private class func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {
115+
private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {
116+
#if !os(iOS)
117+
return nil
118+
#else
170119
if let mfaResponse = response as? AuthMFAResponse,
171120
mfaResponse.idToken == nil,
172121
let enrollments = mfaResponse.mfaInfo {
@@ -189,17 +138,15 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
189138
} else {
190139
return nil
191140
}
192-
}
193-
#else
194-
private class func generateMFAError(response: AuthRPCResponse, auth: Auth?) -> Error? {
195-
return nil
196-
}
197-
#endif
141+
#endif // !os(iOS)
142+
}
198143

199-
#if os(iOS)
200-
// Check whether or not the successful response is actually the special case phone
201-
// auth flow that returns a temporary proof and phone number.
202-
private class func phoneCredentialInUse(response: AuthRPCResponse) -> Error? {
144+
// Check whether or not the successful response is actually the special case phone
145+
// auth flow that returns a temporary proof and phone number.
146+
private static func phoneCredentialInUse(response: AuthRPCResponse) -> Error? {
147+
#if !os(iOS)
148+
return nil
149+
#else
203150
if let phoneAuthResponse = response as? VerifyPhoneNumberResponse,
204151
let phoneNumber = phoneAuthResponse.phoneNumber,
205152
phoneNumber.count > 0,
@@ -214,12 +161,8 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
214161
} else {
215162
return nil
216163
}
217-
}
218-
#else
219-
private class func phoneCredentialInUse(response: AuthRPCResponse) -> Error? {
220-
return nil
221-
}
222-
#endif
164+
#endif // !os(iOS)
165+
}
223166

224167
/// Calls the RPC using HTTP request.
225168
///
@@ -308,7 +251,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
308251
}
309252
dictionary = decodedDictionary
310253

311-
let response = T.Response()
254+
var response = T.Response()
312255

313256
// At this point we either have an error with successfully decoded
314257
// details in the body, or we have a response which must pass further
@@ -318,7 +261,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
318261
if error != nil {
319262
if let errorDictionary = dictionary["error"] as? [String: AnyHashable] {
320263
if let errorMessage = errorDictionary["message"] as? String {
321-
if let clientError = AuthBackendRPCImplementation.clientError(
264+
if let clientError = Self.clientError(
322265
withServerErrorMessage: errorMessage,
323266
errorDictionary: errorDictionary,
324267
response: response,
@@ -351,7 +294,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
351294
if let verifyAssertionRequest = request as? VerifyAssertionRequest {
352295
if verifyAssertionRequest.returnIDPCredential {
353296
if let errorMessage = dictionary["errorMessage"] as? String {
354-
if let clientError = AuthBackendRPCImplementation.clientError(
297+
if let clientError = Self.clientError(
355298
withServerErrorMessage: errorMessage,
356299
errorDictionary: dictionary,
357300
response: response,
@@ -365,10 +308,10 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
365308
return response
366309
}
367310

368-
private class func clientError(withServerErrorMessage serverErrorMessage: String,
369-
errorDictionary: [String: Any],
370-
response: AuthRPCResponse,
371-
error: Error?) -> Error? {
311+
private static func clientError(withServerErrorMessage serverErrorMessage: String,
312+
errorDictionary: [String: Any],
313+
response: AuthRPCResponse,
314+
error: Error?) -> Error? {
372315
let split = serverErrorMessage.split(separator: ":")
373316
let shortErrorMessage = split.first?.trimmingCharacters(in: .whitespacesAndNewlines)
374317
let serverDetailErrorMessage = String(split.count > 1 ? split[1] : "")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseCore
16+
import FirebaseCoreExtension
17+
import Foundation
18+
#if COCOAPODS
19+
import GTMSessionFetcher
20+
#else
21+
import GTMSessionFetcherCore
22+
#endif
23+
24+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
25+
protocol AuthBackendRPCIssuerProtocol {
26+
/// Asynchronously send a HTTP request.
27+
/// - Parameter request: The request to be made.
28+
/// - Parameter body: Request body.
29+
/// - Parameter contentType: Content type of the body.
30+
/// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously
31+
/// on the auth global work queue in the future.
32+
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
33+
body: Data?,
34+
contentType: String) async -> (Data?, Error?)
35+
}
36+
37+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
38+
class AuthBackendRPCIssuer: AuthBackendRPCIssuerProtocol {
39+
let fetcherService: GTMSessionFetcherService
40+
41+
init() {
42+
fetcherService = GTMSessionFetcherService()
43+
fetcherService.userAgent = AuthBackend.authUserAgent()
44+
fetcherService.callbackQueue = kAuthGlobalWorkQueue
45+
46+
// Avoid reusing the session to prevent
47+
// https://github.com/firebase/firebase-ios-sdk/issues/1261
48+
fetcherService.reuseSession = false
49+
}
50+
51+
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
52+
body: Data?,
53+
contentType: String) async -> (Data?, Error?) {
54+
let requestConfiguration = request.requestConfiguration()
55+
let request = await AuthBackend.request(withURL: request.requestURL(),
56+
contentType: contentType,
57+
requestConfiguration: requestConfiguration)
58+
let fetcher = fetcherService.fetcher(with: request)
59+
if let _ = requestConfiguration.emulatorHostAndPort {
60+
fetcher.allowLocalhostRequest = true
61+
fetcher.allowedInsecureSchemes = ["http"]
62+
}
63+
fetcher.bodyData = body
64+
65+
return await withUnsafeContinuation { continuation in
66+
fetcher.beginFetch { data, error in
67+
continuation.resume(returning: (data, error))
68+
}
69+
}
70+
}
71+
}

FirebaseAuth/Sources/Swift/Backend/AuthRPCResponse.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
import Foundation
1616

17-
protocol AuthRPCResponse {
17+
protocol AuthRPCResponse: Sendable {
1818
/// Bare initializer for a response.
1919
init()
2020

2121
/// Sets the response instance from the decoded JSON response.
2222
/// - Parameter dictionary: The dictionary decoded from HTTP JSON response.
2323
/// - Parameter error: An out field for an error which occurred constructing the request.
2424
/// - Returns: Whether the operation was successful or not.
25-
func setFields(dictionary: [String: AnyHashable]) throws
25+
mutating func setFields(dictionary: [String: AnyHashable]) throws
2626

2727
/// This optional method allows response classes to create client errors given a short error
2828
/// message and a detail error message from the server.

FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIResponse.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import Foundation
1616

1717
/// Represents the parameters for the createAuthUri endpoint.
1818
/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri
19-
20-
class CreateAuthURIResponse: AuthRPCResponse {
19+
struct CreateAuthURIResponse: AuthRPCResponse {
2120
/// The URI used by the IDP to authenticate the user.
2221
var authURI: String?
2322

@@ -36,10 +35,7 @@ class CreateAuthURIResponse: AuthRPCResponse {
3635
/// A list of sign-in methods available for the passed identifier.
3736
var signinMethods: [String] = []
3837

39-
/// Bare initializer.
40-
required init() {}
41-
42-
func setFields(dictionary: [String: AnyHashable]) throws {
38+
mutating func setFields(dictionary: [String: AnyHashable]) throws {
4339
providerID = dictionary["providerId"] as? String
4440
authURI = dictionary["authUri"] as? String
4541
registered = dictionary["registered"] as? Bool ?? false

FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountResponse.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ import Foundation
1717
/// Represents the response from the deleteAccount endpoint.
1818
///
1919
/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount
20-
class DeleteAccountResponse: AuthRPCResponse {
21-
required init() {}
22-
23-
func setFields(dictionary: [String: AnyHashable]) throws {}
20+
struct DeleteAccountResponse: AuthRPCResponse {
21+
mutating func setFields(dictionary: [String: AnyHashable]) throws {}
2422
}

FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInResponse.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
import Foundation
1616

1717
/// Represents the response from the emailLinkSignin endpoint.
18-
class EmailLinkSignInResponse: AuthRPCResponse, AuthMFAResponse {
19-
required init() {}
20-
18+
struct EmailLinkSignInResponse: AuthRPCResponse, AuthMFAResponse {
2119
/// The ID token in the email link sign-in response.
2220
private(set) var idToken: String?
2321

@@ -42,7 +40,7 @@ class EmailLinkSignInResponse: AuthRPCResponse, AuthMFAResponse {
4240
/// Info on which multi-factor authentication providers are enabled.
4341
private(set) var mfaInfo: [AuthProtoMFAEnrollment]?
4442

45-
func setFields(dictionary: [String: AnyHashable]) throws {
43+
mutating func setFields(dictionary: [String: AnyHashable]) throws {
4644
email = dictionary["email"] as? String
4745
idToken = dictionary["idToken"] as? String
4846
isNewUser = dictionary["isNewUser"] as? Bool ?? false

0 commit comments

Comments
 (0)