diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index 711c60a0680..6c6dfd23a86 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -32,11 +32,19 @@ enum AuthMenu: String { case custom case initRecaptcha case customAuthDomain - - /// More intuitively named getter for `rawValue`. + case getToken + case getTokenForceRefresh + case addAuthStateChangeListener + case removeLastAuthStateChangeListener + case addIdTokenChangeListener + case removeLastIdTokenChangeListener + case verifyClient + case deleteApp + + // More intuitively named getter for `rawValue`. var id: String { rawValue } - /// The UI friendly name of the `AuthMenu`. Used for display. + // The UI friendly name of the `AuthMenu`. Used for display. var name: String { switch self { case .settings: @@ -71,11 +79,27 @@ enum AuthMenu: String { return "Initialize reCAPTCHA Enterprise" case .customAuthDomain: return "Set Custom Auth Domain" + case .getToken: + return "Get Token" + case .getTokenForceRefresh: + return "Get Token Force Refresh" + case .addAuthStateChangeListener: + return "Add Auth State Change Listener" + case .removeLastAuthStateChangeListener: + return "Remove Last Auth State Change Listener" + case .addIdTokenChangeListener: + return "Add ID Token Change Listener" + case .removeLastIdTokenChangeListener: + return "Remove Last ID Token Change Listener" + case .verifyClient: + return "Verify Client" + case .deleteApp: + return "Delete App" } } - /// Failable initializer to create an `AuthMenu` from it's corresponding `name` value. - /// - Parameter rawValue: String value representing `AuthMenu`'s name or type. + // Failable initializer to create an `AuthMenu` from its corresponding `name` value. + // - Parameter rawValue: String value representing `AuthMenu`'s name or type. init?(rawValue: String) { switch rawValue { case "Settings": @@ -110,7 +134,24 @@ enum AuthMenu: String { self = .initRecaptcha case "Set Custom Auth Domain": self = .customAuthDomain - default: return nil + case "Get Token": + self = .getToken + case "Get Token Force Refresh": + self = .getTokenForceRefresh + case "Add Auth State Change Listener": + self = .addAuthStateChangeListener + case "Remove Last Auth State Change Listener": + self = .removeLastAuthStateChangeListener + case "Add ID Token Change Listener": + self = .addIdTokenChangeListener + case "Remove Last ID Token Change Listener": + self = .removeLastIdTokenChangeListener + case "Verify Client": + self = .verifyClient + case "Delete App": + self = .deleteApp + default: + return nil } } } @@ -172,9 +213,24 @@ extension AuthMenu: DataSourceProvidable { return Section(headerDescription: header, items: [item]) } + static var appSection: Section { + let header = "APP" + let items: [Item] = [ + Item(title: getToken.name), + Item(title: getTokenForceRefresh.name), + Item(title: addAuthStateChangeListener.name), + Item(title: removeLastAuthStateChangeListener.name), + Item(title: addIdTokenChangeListener.name), + Item(title: removeLastIdTokenChangeListener.name), + Item(title: verifyClient.name), + Item(title: deleteApp.name), + ] + return Section(headerDescription: header, items: items) + } + static var sections: [Section] { [settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection, - customAuthDomainSection] + customAuthDomainSection, appSection] } static var authLinkSections: [Section] { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift index 4f57b055574..4ce45df4020 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift @@ -66,6 +66,15 @@ extension User: DataSourceProvidable { // MARK: - UIKit Extensions public extension UIViewController { + func displayInfo(title: String, message: String, style: UIAlertController.Style) { + let alert = UIAlertController(title: title, message: message, preferredStyle: style) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + + DispatchQueue.main.async { // Ensure UI updates on the main thread + self.present(alert, animated: true, completion: nil) + } + } + func displayError(_ error: Error?, from function: StaticString = #function) { guard let error = error else { return } print("ⓧ Error in \(function): \(error.localizedDescription)") diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index 20a00d3b086..7f7350afd1f 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -14,7 +14,7 @@ // For Sign in with Facebook import FBSDKLoginKit -import FirebaseAuth +@testable import FirebaseAuth // [START auth_import] import FirebaseCore @@ -33,6 +33,8 @@ private let kFacebookAppID = "ENTER APP ID HERE" class AuthViewController: UIViewController, DataSourceProviderDelegate { var dataSourceProvider: DataSourceProvider! + var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = [] + var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = [] override func loadView() { view = UITableView(frame: .zero, style: .insetGrouped) @@ -95,6 +97,30 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .customAuthDomain: performCustomAuthDomainFlow() + + case .getToken: + getUserTokenResult(force: false) + + case .getTokenForceRefresh: + getUserTokenResult(force: true) + + case .addAuthStateChangeListener: + addAuthStateListener() + + case .removeLastAuthStateChangeListener: + removeAuthStateListener() + + case .addIdTokenChangeListener: + addIDTokenListener() + + case .removeLastIdTokenChangeListener: + removeIDTokenListener() + + case .verifyClient: + verifyClient() + + case .deleteApp: + deleteApp() } } @@ -316,6 +342,142 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { present(prompt, animated: true) } + private func getUserTokenResult(force: Bool) { + guard let currentUser = Auth.auth().currentUser else { + print("Error: No user logged in") + return + } + + currentUser.getIDTokenResult(forcingRefresh: force, completion: { tokenResult, error in + if error != nil { + print("Error: Error refreshing token") + return // Handle error case, returning early + } + + if let tokenResult = tokenResult, let claims = tokenResult.claims as? [String: Any] { + var message = "Token refresh succeeded\n\n" + for (key, value) in claims { + message += "\(key): \(value)\n" + } + self.displayInfo(title: "Info", message: message, style: .alert) + } else { + print("Error: Unable to access claims.") + } + }) + } + + private func addAuthStateListener() { + weak var weakSelf = self + let index = authStateDidChangeListeners.count + print("Auth State Did Change Listener #\(index) was added.") + let handle = Auth.auth().addStateDidChangeListener { [weak weakSelf] auth, user in + guard weakSelf != nil else { return } + print("Auth State Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "nil")'") + } + authStateDidChangeListeners.append(handle) + } + + private func removeAuthStateListener() { + guard !authStateDidChangeListeners.isEmpty else { + print("No remaining Auth State Did Change Listeners.") + return + } + let index = authStateDidChangeListeners.count - 1 + let handle = authStateDidChangeListeners.last! + Auth.auth().removeStateDidChangeListener(handle) + authStateDidChangeListeners.removeLast() + print("Auth State Did Change Listener #\(index) was removed.") + } + + private func addIDTokenListener() { + weak var weakSelf = self + let index = IDTokenDidChangeListeners.count + print("ID Token Did Change Listener #\(index) was added.") + let handle = Auth.auth().addIDTokenDidChangeListener { [weak weakSelf] auth, user in + guard weakSelf != nil else { return } + print("ID Token Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "")'.") + } + IDTokenDidChangeListeners.append(handle) + } + + func removeIDTokenListener() { + guard !IDTokenDidChangeListeners.isEmpty else { + print("No remaining ID Token Did Change Listeners.") + return + } + let index = IDTokenDidChangeListeners.count - 1 + let handle = IDTokenDidChangeListeners.last! + Auth.auth().removeIDTokenDidChangeListener(handle) + IDTokenDidChangeListeners.removeLast() + print("ID Token Did Change Listener #\(index) was removed.") + } + + func verifyClient() { + AppManager.shared.auth().tokenManager.getTokenInternal { token, error in + if token == nil { + print("Verify iOS Client failed.") + return + } + let request = VerifyClientRequest( + withAppToken: token?.string, + isSandbox: token?.type == .sandbox, + requestConfiguration: AppManager.shared.auth().requestConfiguration + ) + + Task { + do { + let verifyResponse = try await AuthBackend.call(with: request) + + guard let receipt = verifyResponse.receipt, + let timeoutDate = verifyResponse.suggestedTimeOutDate else { + print("Internal Auth Error: invalid VerifyClientResponse.") + return + } + + let timeout = timeoutDate.timeIntervalSinceNow + do { + let credential = await AppManager.shared.auth().appCredentialManager + .didStartVerification( + withReceipt: receipt, + timeout: timeout + ) + + guard credential.secret != nil else { + print("Failed to receive remote notification to verify App ID.") + return + } + + let testPhoneNumber = "+16509964692" + let request = SendVerificationCodeRequest( + phoneNumber: testPhoneNumber, + codeIdentity: CodeIdentity.credential(credential), + requestConfiguration: AppManager.shared.auth().requestConfiguration + ) + + do { + _ = try await AuthBackend.call(with: request) + print("Verify iOS client succeeded") + } catch { + print("Verify iOS Client failed: \(error.localizedDescription)") + } + } + } catch { + print("Verify iOS Client failed: \(error.localizedDescription)") + } + } + } + } + + func deleteApp() { + AppManager.shared.app.delete { success in + if success { + print("App deleted successfully.") + } else { + print("Failed to delete app.") + } + } + } + // MARK: - Private Helpers private func configureDataSourceProvider() { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift index 8757386f24d..3ff02c0a481 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift @@ -272,6 +272,15 @@ extension AuthSettings: DataSourceProvidable { func appCredentialString() -> String { if let credential = AppManager.shared.auth().appCredentialManager.credential { + let message = "receipt: \(credential.receipt)\nsecret: \(credential.secret)" + + showPromptWithTitle("Clear App Credential?", message: message, + showCancelButton: true) { userPressedOK, _ in + if userPressedOK { + AppManager.shared.auth().appCredentialManager.clearCredential() + } + } + } let truncatedReceipt = truncatedString(string: credential.receipt, length: 13) let truncatedSecret = truncatedString(string: credential.secret ?? "", length: 13) return "\(truncatedReceipt)/\(truncatedSecret)"