diff --git a/FirebaseAuth/CHANGELOG.md b/FirebaseAuth/CHANGELOG.md index 143e9dfedc6..db83371c45e 100644 --- a/FirebaseAuth/CHANGELOG.md +++ b/FirebaseAuth/CHANGELOG.md @@ -12,6 +12,7 @@ off of the main actor or main thread. - [fixed] Simplified completion handler memory management in Auth interop (#14962). +- [added] Added TOTP support for macOS. # 11.15.0 - [fixed] Fixed `Sendable` warnings introduced in the Xcode 26 beta. (#14996) diff --git a/FirebaseAuth/README.md b/FirebaseAuth/README.md index 1dbb6f9cbc3..b055658e4c9 100644 --- a/FirebaseAuth/README.md +++ b/FirebaseAuth/README.md @@ -1,4 +1,4 @@ -# Firebase Auth for iOS +# Firebase Auth for iOS and macOS Firebase Auth enables apps to easily support multiple authentication options for their end users. diff --git a/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m b/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m index 4862a485884..85648f2fc3b 100644 --- a/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m +++ b/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_OSX #import diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h index aae0db8ab6f..da05c8309ce 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -22,27 +22,26 @@ NS_ASSUME_NONNULL_BEGIN /** @typedef FIRMultiFactorSessionCallback @brief The callback that triggered when a developer calls `getSessionWithCompletion`. - This type is available on iOS only. + This type is available on iOS and macOS. @param session The multi factor session returned, if any. @param error The error which occurred, if any. */ typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable session, NSError *_Nullable error) - NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.") - API_UNAVAILABLE(macos, tvos, watchos); + NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.") API_UNAVAILABLE(tvos, watchos); /** @brief The string identifier for using phone as a second factor. - This constant is available on iOS only. + This constant is available on iOS and macOS. */ extern NSString *const _Nonnull FIRPhoneMultiFactorID NS_SWIFT_NAME(PhoneMultiFactorID) - API_UNAVAILABLE(macos, tvos, watchos); + API_UNAVAILABLE(tvos, watchos); /** @brief The string identifier for using TOTP as a second factor. - This constant is available on iOS only. + This constant is available on iOS and macOS. */ extern NSString *const _Nonnull FIRTOTPMultiFactorID NS_SWIFT_NAME(TOTPMultiFactorID) - API_UNAVAILABLE(macos, tvos, watchos); + API_UNAVAILABLE(tvos, watchos); NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRTOTPSecret.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRTOTPSecret.h new file mode 100644 index 00000000000..d0a1201cdc8 --- /dev/null +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRTOTPSecret.h @@ -0,0 +1,17 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@class FIRTOTPSecret; \ No newline at end of file diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h index 3917dd8e434..845e534ad4e 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h @@ -26,4 +26,5 @@ #import "FIRGoogleAuthProvider.h" #import "FIRMultiFactor.h" #import "FIRPhoneAuthProvider.h" +#import "FIRTOTPSecret.h" #import "FIRTwitterAuthProvider.h" diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index 7a0c39340ae..55780c1ca89 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -99,9 +99,7 @@ final class AuthBackend: AuthBackendProtocol { } private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? { - #if !os(iOS) - return nil - #else + #if os(iOS) || os(macOS) if let mfaResponse = response as? AuthMFAResponse, mfaResponse.idToken == nil, let enrollments = mfaResponse.mfaInfo { @@ -124,7 +122,9 @@ final class AuthBackend: AuthBackendProtocol { } else { return nil } - #endif // !os(iOS) + #else + return nil + #endif // os(iOS) || os(macOS) } // Check whether or not the successful response is actually the special case phone diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift index 17ec6b18731..e227117c981 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift @@ -14,7 +14,7 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension MultiFactor: NSSecureCoding {} @@ -22,7 +22,7 @@ import Foundation /// The interface defining the multi factor related properties and operations pertaining to a /// user. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactor) open class MultiFactor: NSObject { @objc open var enrolledFactors: [MultiFactorInfo] diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift index ff3edc94dd0..edfad7aaa58 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift @@ -14,12 +14,12 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The base class for asserting ownership of a second factor. This is equivalent to the /// AuthCredential class. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRMultiFactorAssertion) open class MultiFactorAssertion: NSObject { /// The second factor identifier for this opaque object asserting a second factor. @objc open var factorID: String diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift index 839e405fc05..a1b134a7265 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift @@ -16,12 +16,12 @@ import Foundation // TODO(Swift 6 Breaking): Make checked Sendable. -#if os(iOS) +#if os(iOS) || os(macOS) extension MultiFactorInfo: NSSecureCoding {} /// Safe public structure used to represent a second factor entity from a client perspective. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRMultiFactorInfo) open class MultiFactorInfo: NSObject, @unchecked Sendable { /// The multi-factor enrollment ID. @objc(UID) public let uid: String diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift index 223c1f9f5f5..374f86de88c 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift @@ -14,12 +14,12 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The subclass of base class `MultiFactorAssertion`, used to assert ownership of a phone /// second factor. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactorResolver) open class MultiFactorResolver: NSObject { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift index 33f7ef927ce..d22578a291a 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift @@ -14,7 +14,7 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// Opaque object that identifies the current session to enroll a second factor or to /// complete sign in when previously enrolled. @@ -23,7 +23,7 @@ import Foundation /// or to complete sign in when previously enrolled. It contains additional context on the /// existing user, notably the confirmation that the user passed the first factor challenge. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactorSession) open class MultiFactorSession: NSObject { /// The ID token for an enroll flow. This has to be retrieved after recent authentication. diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift index 999809e4bd3..941b0c244f4 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift @@ -14,12 +14,12 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone /// second factor. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneMultiFactorAssertion) open class PhoneMultiFactorAssertion: MultiFactorAssertion { var authCredential: PhoneAuthCredential? diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift index cd213c14196..74c50abfb96 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift @@ -14,14 +14,14 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The data structure used to help initialize an assertion for a second factor entity to the /// Firebase Auth/CICP server. /// /// Depending on the type of second factor, this will help generate the assertion. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneMultiFactorGenerator) open class PhoneMultiFactorGenerator: NSObject { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift index b847407c48a..4ec36505dac 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift @@ -16,13 +16,13 @@ import Foundation // TODO(Swift 6 Breaking): Make checked Sendable. -#if os(iOS) +#if os(iOS) || os(macOS) /// Extends the MultiFactorInfo class for phone number second factors. /// /// The identifier of this second factor is "phone". /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRPhoneMultiFactorInfo) open class PhoneMultiFactorInfo: MultiFactorInfo, @unchecked Sendable { /// The string identifier for using phone as a second factor. diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift index b5b1c43f3d6..dd64e4319af 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift @@ -14,7 +14,7 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) enum SecretOrID { case secret(TOTPSecret) @@ -24,7 +24,7 @@ import Foundation /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP /// (Time-based One Time Password) second factor. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRTOTPMultiFactorAssertion) open class TOTPMultiFactorAssertion: MultiFactorAssertion { let oneTimePassword: String let secretOrID: SecretOrID diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift index bf3b07634ca..c24dedb3b22 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift @@ -14,13 +14,13 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The data structure used to help initialize an assertion for a second factor entity to the /// Firebase Auth/CICP server. Depending on the type of second factor, this will help generate /// the assertion. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRTOTPMultiFactorGenerator) open class TOTPMultiFactorGenerator: NSObject { /// Creates a TOTP secret as part of enrolling a TOTP second factor. Used for generating a diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift index dbb2eeb7042..b252f4fd8c6 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift @@ -17,13 +17,13 @@ import Foundation // TODO(Swift 6 Breaking): Make checked Sendable. Also, does this need // to be public? -#if os(iOS) +#if os(iOS) || os(macOS) /// Extends the MultiFactorInfo class for time based one-time password second factors. /// /// The identifier of this second factor is "totp". /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. class TOTPMultiFactorInfo: MultiFactorInfo, @unchecked Sendable { /// Initialize the AuthProtoMFAEnrollment instance with proto. /// - Parameter proto: AuthProtoMFAEnrollment proto object. diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift index 9791b974e52..8ed3a2dffff 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift @@ -19,13 +19,17 @@ import Foundation internal import GoogleUtilities_Environment #endif -#if os(iOS) - import UIKit +#if os(iOS) || os(macOS) + #if os(iOS) + import UIKit + #elseif os(macOS) + import AppKit + #endif /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP /// (Time-based One Time Password) second factor. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRTOTPSecret) open class TOTPSecret: NSObject { /// Returns the shared secret key/seed used to generate time-based one-time passwords. @objc open func sharedSecretKey() -> String { @@ -57,24 +61,34 @@ import Foundation @MainActor @objc(openInOTPAppWithQRCodeURL:) open func openInOTPApp(withQRCodeURL qrCodeURL: String) { if GULAppEnvironmentUtil.isAppExtension() { - // iOS App extensions should not call [UIApplication sharedApplication], even if - // UIApplication responds to it. + // App extensions should not call [UIApplication sharedApplication] or [NSWorkspace + // sharedWorkspace], + // even if they respond to it. return } - // Using reflection here to avoid build errors in extensions. - let sel = NSSelectorFromString("sharedApplication") - guard UIApplication.responds(to: sel), - let rawApplication = UIApplication.perform(sel), - let application = rawApplication.takeUnretainedValue() as? UIApplication else { - return - } - if let url = URL(string: qrCodeURL), application.canOpenURL(url) { - application.open(url, options: [:], completionHandler: nil) - } else { - AuthLog.logError(code: "I-AUT000019", - message: "URL: \(qrCodeURL) cannot be opened") - } + #if os(iOS) + // Using reflection here to avoid build errors in extensions. + let sel = NSSelectorFromString("sharedApplication") + guard UIApplication.responds(to: sel), + let rawApplication = UIApplication.perform(sel), + let application = rawApplication.takeUnretainedValue() as? UIApplication else { + return + } + if let url = URL(string: qrCodeURL), application.canOpenURL(url) { + application.open(url, options: [:], completionHandler: nil) + } else { + AuthLog.logError(code: "I-AUT000019", + message: "URL: \(qrCodeURL) cannot be opened") + } + #elseif os(macOS) + if let url = URL(string: qrCodeURL) { + NSWorkspace.shared.open(url) + } else { + AuthLog.logError(code: "I-AUT000019", + message: "URL: \(qrCodeURL) cannot be opened") + } + #endif } /// Shared secret key/seed used for enrolling in TOTP MFA and generating OTPs. diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 4ef324e177c..ea006baa418 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -58,10 +58,10 @@ extension User: NSSecureCoding {} /// The tenant ID of the current user. `nil` if none is available. @objc public private(set) var tenantID: String? - #if os(iOS) + #if os(iOS) || os(macOS) /// Multi factor object associated with the user. /// - /// This property is available on iOS only. + /// This property is available on iOS and macOS. @objc public private(set) var multiFactor: MultiFactor #endif @@ -1066,7 +1066,7 @@ extension User: NSSecureCoding {} isEmailVerified = false metadata = UserMetadata(withCreationDate: nil, lastSignInDate: nil) tenantID = nil - #if os(iOS) + #if os(iOS) || os(macOS) multiFactor = MultiFactor(withMFAEnrollments: []) #endif uid = "" @@ -1297,7 +1297,7 @@ extension User: NSSecureCoding {} } } providerDataRaw = providerData - #if os(iOS) + #if os(iOS) || os(macOS) if let enrollments = user.mfaEnrollments { multiFactor = MultiFactor(withMFAEnrollments: enrollments) } @@ -1718,7 +1718,7 @@ extension User: NSSecureCoding {} coder.encode(auth.requestConfiguration.appID, forKey: kFirebaseAppIDCodingKey) } coder.encode(tokenService, forKey: kTokenServiceCodingKey) - #if os(iOS) + #if os(iOS) || os(macOS) coder.encode(multiFactor, forKey: kMultiFactorCodingKey) #endif } @@ -1747,7 +1747,7 @@ extension User: NSSecureCoding {} as? [String: UserInfoImpl] let metadata = coder.decodeObject(of: UserMetadata.self, forKey: kMetadataCodingKey) let tenantID = coder.decodeObject(of: NSString.self, forKey: kTenantIDCodingKey) as? String - #if os(iOS) + #if os(iOS) || os(macOS) let multiFactor = coder.decodeObject(of: MultiFactor.self, forKey: kMultiFactorCodingKey) #endif self.tokenService = tokenService @@ -1778,7 +1778,7 @@ extension User: NSSecureCoding {} backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer()) userProfileUpdate = UserProfileUpdate() - #if os(iOS) + #if os(iOS) || os(macOS) self.multiFactor = multiFactor ?? MultiFactor() super.init() multiFactor?.user = self diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift index 5c78b223ab4..17d76c810a7 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift @@ -568,7 +568,7 @@ class AuthErrorUtils { return error(code: .blockingCloudFunctionError, message: errorMessage) } - #if os(iOS) + #if os(iOS) || os(macOS) static func secondFactorRequiredError(pendingCredential: String?, hints: [MultiFactorInfo], auth: Auth) @@ -581,7 +581,7 @@ class AuthErrorUtils { return error(code: .secondFactorRequired, userInfo: userInfo) } - #endif // os(iOS) + #endif // os(iOS) || os(macOS) static func recaptchaSDKNotLinkedError() -> Error { // TODO(ObjC): point the link to GCIP doc once available. diff --git a/FirebaseAuth/Tests/Unit/ObjCAPITests.m b/FirebaseAuth/Tests/Unit/ObjCAPITests.m index 6784beb8548..d6a528d8930 100644 --- a/FirebaseAuth/Tests/Unit/ObjCAPITests.m +++ b/FirebaseAuth/Tests/Unit/ObjCAPITests.m @@ -372,7 +372,7 @@ - (void)FIRGoogleAuthProvider_h { accessToken:@"token"]; } -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_OSX - (void)FIRMultiFactor_h:(FIRMultiFactor *)mf mfa:(FIRMultiFactorAssertion *)mfa { [mf getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable credential, NSError *_Nullable error){ @@ -466,7 +466,9 @@ - (void)FIRPhoneAuthProvider_h:(FIRPhoneAuthCredential *)credential { - (void)phoneMultiFactorInfo:(FIRPhoneMultiFactorInfo *)info { __unused NSString *s = [info phoneNumber]; } +#endif +#if TARGET_OS_IOS || TARGET_OS_OSX - (void)FIRTOTPSecret_h:(FIRTOTPSecret *)secret { NSString *s = [secret sharedSecretKey]; s = [secret generateQRCodeURLWithAccountName:@"name" issuer:@"issuer"]; @@ -571,7 +573,7 @@ - (void)userProperties:(FIRUser *)user { b = [user isEmailVerified]; __unused NSArray *> *userInfo = [user providerData]; __unused FIRUserMetadata *meta = [user metadata]; -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_OSX __unused FIRMultiFactor *mf = [user multiFactor]; #endif NSString *s = [user refreshToken]; diff --git a/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m b/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m index 59fe0718b5e..62ab96666c5 100644 --- a/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m +++ b/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m @@ -41,9 +41,11 @@ - (void)GlobalSymbolBuildTest { s = FIRGoogleAuthSignInMethod; #if TARGET_OS_IOS s = FIRPhoneMultiFactorID; - s = FIRTOTPMultiFactorID; s = FIRPhoneAuthProviderID; s = FIRPhoneAuthSignInMethod; +#endif +#if TARGET_OS_IOS || TARGET_OS_OSX + s = FIRTOTPMultiFactorID; #endif s = FIRTwitterAuthProviderID; s = FIRTwitterAuthSignInMethod; diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index db002ccd599..fc6c2f466da 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -548,7 +548,9 @@ class AuthAPI_hOnlyTests: XCTestCase { func phoneMultiFactorInfo(mfi: PhoneMultiFactorInfo) { let _: String = mfi.phoneNumber } + #endif + #if os(iOS) || os(macOS) func FIRTOTPSecret_h(session: MultiFactorSession) async throws { let obj = try await TOTPMultiFactorGenerator.generateSecret(with: session) _ = obj.sharedSecretKey() @@ -638,7 +640,7 @@ class AuthAPI_hOnlyTests: XCTestCase { let _: Bool = user.isEmailVerified let _: [UserInfo] = user.providerData let _: UserMetadata = user.metadata - #if os(iOS) + #if os(iOS) || os(macOS) let _: MultiFactor = user.multiFactor #endif if let _: String = user.refreshToken, diff --git a/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift b/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift index f0956d54aa3..8d863cb1a58 100644 --- a/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift +++ b/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift @@ -38,9 +38,10 @@ class SwiftGlobalTests: XCTestCase { let _: String = GitHubAuthSignInMethod let _: String = GoogleAuthProviderID let _: String = GoogleAuthSignInMethod - #if os(iOS) - let _: String = PhoneMultiFactorID + #if os(iOS) || os(macOS) let _: String = TOTPMultiFactorID + #endif + #if os(iOS) let _: String = PhoneAuthProviderID let _: String = PhoneAuthSignInMethod #endif diff --git a/FirebaseAuth/Tests/Unit/UserTests.swift b/FirebaseAuth/Tests/Unit/UserTests.swift index c610e04a0bc..24f426d1bd8 100644 --- a/FirebaseAuth/Tests/Unit/UserTests.swift +++ b/FirebaseAuth/Tests/Unit/UserTests.swift @@ -135,6 +135,27 @@ class UserTests: RPCBaseTests { ]) #endif + var mfaInfo: [[AnyHashable: AnyHashable]] = [] + + #if os(iOS) + mfaInfo.append([ + "phoneInfo": kPhoneInfo, + "mfaEnrollmentId": kEnrollmentID, + "displayName": kDisplayName, + "enrolledAt": kEnrolledAt, + ]) + #endif + + #if os(iOS) || os(macOS) + mfaInfo.append([ + // In practice, this will be an empty dictionary. + "totpInfo": [AnyHashable: AnyHashable](), + "mfaEnrollmentId": kEnrollmentID, + "displayName": kDisplayName, + "enrolledAt": kEnrolledAt, + ]) + #endif + rpcIssuer?.fakeGetAccountProviderJSON = [[ kProviderUserInfoKey: providerUserInfos, kLocalIDKey: kLocalID, @@ -146,21 +167,7 @@ class UserTests: RPCBaseTests { "phoneNumber": kPhoneNumber, "createdAt": String(Int(kCreationDateTimeIntervalInSeconds) * 1000), // to nanoseconds "lastLoginAt": String(Int(kLastSignInDateTimeIntervalInSeconds) * 1000), - "mfaInfo": [ - [ - "phoneInfo": kPhoneInfo, - "mfaEnrollmentId": kEnrollmentID, - "displayName": kDisplayName, - "enrolledAt": kEnrolledAt, - ], - [ - // In practice, this will be an empty dictionary. - "totpInfo": [AnyHashable: AnyHashable](), - "mfaEnrollmentId": kEnrollmentID, - "displayName": kDisplayName, - "enrolledAt": kEnrolledAt, - ] as [AnyHashable: AnyHashable], - ], + "mfaInfo": mfaInfo, ]] let expectation = self.expectation(description: #function) @@ -247,9 +254,12 @@ class UserTests: RPCBaseTests { var encodedClasses = [User.self, NSDictionary.self, NSURL.self, SecureTokenService.self, UserInfoImpl.self, NSDate.self, UserMetadata.self, NSString.self, NSArray.self] - #if os(iOS) + #if os(iOS) || os(macOS) encodedClasses.append(MultiFactor.self) - encodedClasses.append(PhoneMultiFactorInfo.self) + encodedClasses.append(TOTPMultiFactorInfo.self) + #if os(iOS) + encodedClasses.append(PhoneMultiFactorInfo.self) + #endif #endif let unarchivedUser = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject( @@ -370,6 +380,17 @@ class UserTests: RPCBaseTests { XCTAssertEqual("\(date)", kEnrolledAtMatch) } #endif + + #if os(macOS) + // Verify TOTP MultiFactorInfo properties. + let enrolledFactors = try XCTUnwrap(user.multiFactor.enrolledFactors) + XCTAssertEqual(enrolledFactors.count, 1) + XCTAssertEqual(enrolledFactors[0].factorID, PhoneMultiFactorInfo.TOTPMultiFactorID) + XCTAssertEqual(enrolledFactors[0].uid, kEnrollmentID) + XCTAssertEqual(enrolledFactors[0].displayName, self.kDisplayName) + let date = try XCTUnwrap(enrolledFactors[0].enrollmentDate) + XCTAssertEqual("\(date)", kEnrolledAtMatch) + #endif } catch { XCTFail("Caught an error in \(#function): \(error)") } diff --git a/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift b/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift index 4fd850da1e6..b7f8a790dff 100644 --- a/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if os(iOS) || targetEnvironment(macCatalyst) +#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS) import Combine import FirebaseAuth - @available(iOS 13.0, macCatalyst 13.0, *) - @available(macOS, unavailable) + @available(iOS 13.0, macCatalyst 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) public extension MultiFactor { @@ -111,4 +110,4 @@ } } -#endif // os(iOS) || targetEnvironment(macCatalyst) +#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS) diff --git a/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift b/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift index b2ada743c66..a94e3b7cd82 100644 --- a/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if os(iOS) || targetEnvironment(macCatalyst) +#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS) import Combine import FirebaseAuth - @available(iOS 13.0, macCatalyst 13.0, *) - @available(macOS, unavailable) + @available(iOS 13.0, macCatalyst 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) public extension MultiFactorResolver { @@ -43,4 +42,4 @@ } } -#endif // os(iOS) || targetEnvironment(macCatalyst) +#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS)