mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
505 lines
25 KiB
Swift
505 lines
25 KiB
Swift
// Copyright 2023 Google LLC
|
|
//
|
|
// 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.
|
|
|
|
import FirebaseCore
|
|
import Foundation
|
|
|
|
/// A concrete implementation of `AuthProvider` for phone auth providers.
|
|
///
|
|
/// This class is available on iOS only.
|
|
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
@objc(FIRPhoneAuthProvider) open class PhoneAuthProvider: NSObject {
|
|
/// A string constant identifying the phone identity provider.
|
|
@objc public static let id = "phone"
|
|
#if os(iOS)
|
|
/// Returns an instance of `PhoneAuthProvider` for the default `Auth` object.
|
|
@objc(provider) open class func provider() -> PhoneAuthProvider {
|
|
return PhoneAuthProvider(auth: Auth.auth())
|
|
}
|
|
|
|
/// Returns an instance of `PhoneAuthProvider` for the provided `Auth` object.
|
|
/// - Parameter auth: The auth object to associate with the phone auth provider instance.
|
|
@objc(providerWithAuth:)
|
|
open class func provider(auth: Auth) -> PhoneAuthProvider {
|
|
return PhoneAuthProvider(auth: auth)
|
|
}
|
|
|
|
/// Starts the phone number authentication flow by sending a verification code to the
|
|
/// specified phone number.
|
|
///
|
|
/// Possible error codes:
|
|
/// * `AuthErrorCodeCaptchaCheckFailed` - Indicates that the reCAPTCHA token obtained by
|
|
/// the Firebase Auth is invalid or has expired.
|
|
/// * `AuthErrorCodeQuotaExceeded` - Indicates that the phone verification quota for this
|
|
/// project has been exceeded.
|
|
/// * `AuthErrorCodeInvalidPhoneNumber` - Indicates that the phone number provided is invalid.
|
|
/// * `AuthErrorCodeMissingPhoneNumber` - Indicates that a phone number was not provided.
|
|
/// - Parameter phoneNumber: The phone number to be verified.
|
|
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
|
|
/// retained by this method until the completion block is executed.
|
|
/// - Parameter completion: The callback to be invoked when the verification flow is finished.
|
|
@objc(verifyPhoneNumber:UIDelegate:completion:)
|
|
open func verifyPhoneNumber(_ phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate? = nil,
|
|
completion: ((_: String?, _: Error?) -> Void)?) {
|
|
verifyPhoneNumber(phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: nil,
|
|
completion: completion)
|
|
}
|
|
|
|
/// Verify ownership of the second factor phone number by the current user.
|
|
/// - Parameter phoneNumber: The phone number to be verified.
|
|
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
|
|
/// retained by this method until the completion block is executed.
|
|
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
|
|
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
|
|
/// passed the first factor challenge.
|
|
/// - Parameter completion: The callback to be invoked when the verification flow is finished.
|
|
@objc(verifyPhoneNumber:UIDelegate:multiFactorSession:completion:)
|
|
open func verifyPhoneNumber(_ phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate? = nil,
|
|
multiFactorSession: MultiFactorSession? = nil,
|
|
completion: ((_: String?, _: Error?) -> Void)?) {
|
|
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
|
|
urlTypes: auth.mainBundleUrlTypes) else {
|
|
fatalError(
|
|
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
|
|
)
|
|
}
|
|
kAuthGlobalWorkQueue.async {
|
|
Task {
|
|
do {
|
|
let verificationID = try await self.internalVerify(
|
|
phoneNumber: phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: multiFactorSession
|
|
)
|
|
Auth.wrapMainAsync(callback: completion, withParam: verificationID, error: nil)
|
|
} catch {
|
|
Auth.wrapMainAsync(callback: completion, withParam: nil, error: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify ownership of the second factor phone number by the current user.
|
|
/// - Parameter phoneNumber: The phone number to be verified.
|
|
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
|
|
/// retained by this method until the completion block is executed.
|
|
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
|
|
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
|
|
/// passed the first factor challenge.
|
|
/// - Returns: The verification ID
|
|
@available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
|
|
open func verifyPhoneNumber(_ phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate? = nil,
|
|
multiFactorSession: MultiFactorSession? = nil) async throws
|
|
-> String {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.verifyPhoneNumber(phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: multiFactorSession) { result, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else if let result {
|
|
continuation.resume(returning: result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify ownership of the second factor phone number by the current user.
|
|
/// - Parameter multiFactorInfo: The phone multi factor whose number need to be verified.
|
|
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
|
|
/// retained by this method until the completion block is executed.
|
|
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
|
|
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
|
|
/// passed the first factor challenge.
|
|
/// - Parameter completion: The callback to be invoked when the verification flow is finished.
|
|
@objc(verifyPhoneNumberWithMultiFactorInfo:UIDelegate:multiFactorSession:completion:)
|
|
open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
|
|
uiDelegate: AuthUIDelegate? = nil,
|
|
multiFactorSession: MultiFactorSession?,
|
|
completion: ((_: String?, _: Error?) -> Void)?) {
|
|
multiFactorSession?.multiFactorInfo = multiFactorInfo
|
|
verifyPhoneNumber(multiFactorInfo.phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: multiFactorSession,
|
|
completion: completion)
|
|
}
|
|
|
|
/// Verify ownership of the second factor phone number by the current user.
|
|
/// - Parameter multiFactorInfo: The phone multi factor whose number need to be verified.
|
|
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
|
|
/// retained by this method until the completion block is executed.
|
|
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
|
|
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
|
|
/// passed the first factor challenge.
|
|
/// - Returns: The verification ID.
|
|
@available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
|
|
open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
|
|
uiDelegate: AuthUIDelegate? = nil,
|
|
multiFactorSession: MultiFactorSession?) async throws -> String {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.verifyPhoneNumber(with: multiFactorInfo,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: multiFactorSession) { result, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else if let result {
|
|
continuation.resume(returning: result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates an `AuthCredential` for the phone number provider identified by the
|
|
/// verification ID and verification code.
|
|
///
|
|
/// - Parameter verificationID: The verification ID obtained from invoking
|
|
/// verifyPhoneNumber:completion:
|
|
/// - Parameter verificationCode: The verification code obtained from the user.
|
|
/// - Returns: The corresponding phone auth credential for the verification ID and verification
|
|
/// code provided.
|
|
@objc(credentialWithVerificationID:verificationCode:)
|
|
open func credential(withVerificationID verificationID: String,
|
|
verificationCode: String) -> PhoneAuthCredential {
|
|
return PhoneAuthCredential(withProviderID: PhoneAuthProvider.id,
|
|
verificationID: verificationID,
|
|
verificationCode: verificationCode)
|
|
}
|
|
|
|
private func internalVerify(phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate?,
|
|
multiFactorSession: MultiFactorSession? = nil) async throws
|
|
-> String? {
|
|
guard phoneNumber.count > 0 else {
|
|
throw AuthErrorUtils.missingPhoneNumberError(message: nil)
|
|
}
|
|
guard let manager = auth.notificationManager else {
|
|
throw AuthErrorUtils.notificationNotForwardedError()
|
|
}
|
|
guard await manager.checkNotificationForwarding() else {
|
|
throw AuthErrorUtils.notificationNotForwardedError()
|
|
}
|
|
return try await verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: true,
|
|
multiFactorSession: multiFactorSession,
|
|
uiDelegate: uiDelegate)
|
|
}
|
|
|
|
/// Starts the flow to verify the client via silent push notification.
|
|
/// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an
|
|
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
|
|
/// - Parameter phoneNumber: The phone number to be verified.
|
|
/// - Parameter callback: The callback to be invoked on the global work queue when the flow is
|
|
/// finished.
|
|
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
|
|
retryOnInvalidAppCredential: Bool,
|
|
uiDelegate: AuthUIDelegate?) async throws
|
|
-> String? {
|
|
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
|
|
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
|
|
codeIdentity: codeIdentity,
|
|
requestConfiguration: auth
|
|
.requestConfiguration)
|
|
|
|
do {
|
|
let response = try await AuthBackend.call(with: request)
|
|
return response.verificationID
|
|
} catch {
|
|
return try await handleVerifyErrorWithRetry(error: error,
|
|
phoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
|
|
multiFactorSession: nil,
|
|
uiDelegate: uiDelegate)
|
|
}
|
|
}
|
|
|
|
/// Starts the flow to verify the client via silent push notification.
|
|
/// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an
|
|
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
|
|
/// - Parameter phoneNumber: The phone number to be verified.
|
|
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
|
|
retryOnInvalidAppCredential: Bool,
|
|
multiFactorSession session: MultiFactorSession?,
|
|
uiDelegate: AuthUIDelegate?) async throws
|
|
-> String? {
|
|
if let settings = auth.settings,
|
|
settings.isAppVerificationDisabledForTesting {
|
|
let request = SendVerificationCodeRequest(
|
|
phoneNumber: phoneNumber,
|
|
codeIdentity: CodeIdentity.empty,
|
|
requestConfiguration: auth.requestConfiguration
|
|
)
|
|
|
|
let response = try await AuthBackend.call(with: request)
|
|
return response.verificationID
|
|
}
|
|
guard let session else {
|
|
return try await verifyClAndSendVerificationCode(
|
|
toPhoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
|
|
uiDelegate: uiDelegate
|
|
)
|
|
}
|
|
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
|
|
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
|
|
codeIdentity: codeIdentity)
|
|
do {
|
|
if let idToken = session.idToken {
|
|
let request = StartMFAEnrollmentRequest(idToken: idToken,
|
|
enrollmentInfo: startMFARequestInfo,
|
|
requestConfiguration: auth.requestConfiguration)
|
|
let response = try await AuthBackend.call(with: request)
|
|
return response.phoneSessionInfo?.sessionInfo
|
|
} else {
|
|
let request = StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential,
|
|
MFAEnrollmentID: session.multiFactorInfo?.uid,
|
|
signInInfo: startMFARequestInfo,
|
|
requestConfiguration: auth.requestConfiguration)
|
|
|
|
let response = try await AuthBackend.call(with: request)
|
|
return response.responseInfo?.sessionInfo
|
|
}
|
|
} catch {
|
|
return try await handleVerifyErrorWithRetry(
|
|
error: error,
|
|
phoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
|
|
multiFactorSession: session,
|
|
uiDelegate: uiDelegate
|
|
)
|
|
}
|
|
}
|
|
|
|
private func handleVerifyErrorWithRetry(error: Error,
|
|
phoneNumber: String,
|
|
retryOnInvalidAppCredential: Bool,
|
|
multiFactorSession session: MultiFactorSession?,
|
|
uiDelegate: AuthUIDelegate?) async throws -> String? {
|
|
if (error as NSError).code == AuthErrorCode.invalidAppCredential.rawValue {
|
|
if retryOnInvalidAppCredential {
|
|
auth.appCredentialManager.clearCredential()
|
|
return try await verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: false,
|
|
multiFactorSession: session,
|
|
uiDelegate: uiDelegate)
|
|
}
|
|
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: nil, underlyingError: error)
|
|
}
|
|
throw error
|
|
}
|
|
|
|
/// Continues the flow to verify the client via silent push notification.
|
|
private func verifyClient(withUIDelegate uiDelegate: AuthUIDelegate?) async throws
|
|
-> CodeIdentity {
|
|
// Remove the simulator check below after FCM supports APNs in simulators
|
|
#if targetEnvironment(simulator)
|
|
let environment = ProcessInfo().environment
|
|
if environment["XCTestConfigurationFilePath"] == nil {
|
|
return try await CodeIdentity
|
|
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
|
|
}
|
|
#endif
|
|
if let credential = auth.appCredentialManager.credential {
|
|
return CodeIdentity.credential(credential)
|
|
}
|
|
var token: AuthAPNSToken
|
|
do {
|
|
token = try await auth.tokenManager.getToken()
|
|
} catch {
|
|
return try await CodeIdentity
|
|
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
|
|
}
|
|
let request = VerifyClientRequest(withAppToken: token.string,
|
|
isSandbox: token.type == AuthAPNSTokenType.sandbox,
|
|
requestConfiguration: auth.requestConfiguration)
|
|
do {
|
|
let verifyResponse = try await AuthBackend.call(with: request)
|
|
guard let receipt = verifyResponse.receipt,
|
|
let timeout = verifyResponse.suggestedTimeOutDate?.timeIntervalSinceNow else {
|
|
fatalError("Internal Auth Error: invalid VerifyClientResponse")
|
|
}
|
|
let credential = await
|
|
auth.appCredentialManager.didStartVerification(withReceipt: receipt, timeout: timeout)
|
|
if credential.secret == nil {
|
|
AuthLog.logWarning(code: "I-AUT000014", message: "Failed to receive remote " +
|
|
"notification to verify app identity within \(timeout) " +
|
|
"second(s), falling back to reCAPTCHA verification.")
|
|
return try await CodeIdentity
|
|
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
|
|
}
|
|
return CodeIdentity.credential(credential)
|
|
} catch {
|
|
let nserror = error as NSError
|
|
// reCAPTCHA Flow if it's an invalid app credential or a missing app token.
|
|
if (nserror.code == AuthErrorCode.internalError.rawValue &&
|
|
(nserror.userInfo[NSUnderlyingErrorKey] as? NSError)?.code ==
|
|
AuthErrorCode.invalidAppCredential.rawValue) ||
|
|
nserror.code == AuthErrorCode.missingAppToken.rawValue {
|
|
return try await CodeIdentity
|
|
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Continues the flow to verify the client via silent push notification.
|
|
private func reCAPTCHAFlowWithUIDelegate(withUIDelegate uiDelegate: AuthUIDelegate?) async throws
|
|
-> String {
|
|
let eventID = AuthWebUtils.randomString(withLength: 10)
|
|
guard let url = try await reCAPTCHAURL(withEventID: eventID) else {
|
|
fatalError(
|
|
"Internal error: reCAPTCHAURL returned neither a value nor an error. Report issue"
|
|
)
|
|
}
|
|
let callbackMatcher: (URL?) -> Bool = { callbackURL in
|
|
AuthWebUtils.isExpectedCallbackURL(
|
|
callbackURL,
|
|
eventID: eventID,
|
|
authType: self.kAuthTypeVerifyApp,
|
|
callbackScheme: self.callbackScheme
|
|
)
|
|
}
|
|
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.auth.authURLPresenter.present(url,
|
|
uiDelegate: uiDelegate,
|
|
callbackMatcher: callbackMatcher) { callbackURL, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
do {
|
|
try continuation.resume(returning: self.reCAPTCHAToken(forURL: callbackURL))
|
|
} catch {
|
|
continuation.resume(throwing: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses the reCAPTCHA URL and returns the reCAPTCHA token.
|
|
/// - Parameter url: The url to be parsed for a reCAPTCHA token.
|
|
/// - Returns: The reCAPTCHA token if successful.
|
|
private func reCAPTCHAToken(forURL url: URL?) throws -> String {
|
|
guard let url = url else {
|
|
let reason = "Internal Auth Error: nil URL trying to access RECAPTCHA token"
|
|
throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason)
|
|
}
|
|
let actualURLComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
|
if let queryItems = actualURLComponents?.queryItems,
|
|
let deepLinkURL = AuthWebUtils.queryItemValue(name: "deep_link_id", from: queryItems) {
|
|
let deepLinkComponents = URLComponents(string: deepLinkURL)
|
|
if let queryItems = deepLinkComponents?.queryItems {
|
|
if let token = AuthWebUtils.queryItemValue(name: "recaptchaToken", from: queryItems) {
|
|
return token
|
|
}
|
|
if let firebaseError = AuthWebUtils.queryItemValue(
|
|
name: "firebaseError",
|
|
from: queryItems
|
|
) {
|
|
if let errorData = firebaseError.data(using: .utf8) {
|
|
var errorDict: [AnyHashable: Any]?
|
|
do {
|
|
errorDict = try JSONSerialization.jsonObject(with: errorData) as? [AnyHashable: Any]
|
|
} catch {
|
|
throw AuthErrorUtils.JSONSerializationError(underlyingError: error)
|
|
}
|
|
if let errorDict,
|
|
let code = errorDict["code"] as? String,
|
|
let message = errorDict["message"] as? String {
|
|
throw AuthErrorUtils.urlResponseError(code: code, message: message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let reason = "An unknown error occurred with the following response: \(deepLinkURL)"
|
|
throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason)
|
|
}
|
|
let reason = "Failed to get url Components for url: \(url)"
|
|
throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason)
|
|
}
|
|
|
|
/// Constructs a URL used for opening a reCAPTCHA app verification flow using a given event ID.
|
|
/// - Parameter eventID: The event ID used for this purpose.
|
|
private func reCAPTCHAURL(withEventID eventID: String) async throws -> URL? {
|
|
let authDomain = try await AuthWebUtils
|
|
.fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration)
|
|
let bundleID = Bundle.main.bundleIdentifier
|
|
let clientID = auth.app?.options.clientID
|
|
let appID = auth.app?.options.googleAppID
|
|
let apiKey = auth.requestConfiguration.apiKey
|
|
let appCheck = auth.requestConfiguration.appCheck
|
|
var queryItems = [URLQueryItem(name: "apiKey", value: apiKey),
|
|
URLQueryItem(name: "authType", value: kAuthTypeVerifyApp),
|
|
URLQueryItem(name: "ibi", value: bundleID ?? ""),
|
|
URLQueryItem(name: "v", value: AuthBackend.authUserAgent()),
|
|
URLQueryItem(name: "eventId", value: eventID)]
|
|
if usingClientIDScheme {
|
|
queryItems.append(URLQueryItem(name: "clientId", value: clientID))
|
|
} else {
|
|
queryItems.append(URLQueryItem(name: "appId", value: appID))
|
|
}
|
|
if let languageCode = auth.requestConfiguration.languageCode {
|
|
queryItems.append(URLQueryItem(name: "hl", value: languageCode))
|
|
}
|
|
var components = URLComponents(string: "https://\(authDomain)/__/auth/handler?")
|
|
components?.queryItems = queryItems
|
|
|
|
if let appCheck {
|
|
let tokenResult = await appCheck.getToken(forcingRefresh: false)
|
|
if let error = tokenResult.error {
|
|
AuthLog.logWarning(code: "I-AUT000018",
|
|
message: "Error getting App Check token; using placeholder " +
|
|
"token instead. Error: \(error)")
|
|
}
|
|
let appCheckTokenFragment = "fac=\(tokenResult.token)"
|
|
components?.fragment = appCheckTokenFragment
|
|
}
|
|
return components?.url
|
|
}
|
|
|
|
private let auth: Auth
|
|
private let callbackScheme: String
|
|
private let usingClientIDScheme: Bool
|
|
|
|
init(auth: Auth) {
|
|
self.auth = auth
|
|
if let clientID = auth.app?.options.clientID {
|
|
let reverseClientIDScheme = clientID.components(separatedBy: ".").reversed()
|
|
.joined(separator: ".")
|
|
if AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: reverseClientIDScheme,
|
|
urlTypes: auth.mainBundleUrlTypes) {
|
|
callbackScheme = reverseClientIDScheme
|
|
usingClientIDScheme = true
|
|
return
|
|
}
|
|
}
|
|
usingClientIDScheme = false
|
|
if let appID = auth.app?.options.googleAppID {
|
|
let dashedAppID = appID.replacingOccurrences(of: ":", with: "-")
|
|
callbackScheme = "app-\(dashedAppID)"
|
|
return
|
|
}
|
|
callbackScheme = ""
|
|
}
|
|
|
|
private let kAuthTypeVerifyApp = "verifyApp"
|
|
#endif
|
|
}
|