diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift index 5683ed96331..02f2ea3942e 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift @@ -25,8 +25,10 @@ class AppManager { private var otherApp: FirebaseApp var app: FirebaseApp + // Initialise Auth with TenantConfig + let tenantConfig = Auth.TenantConfig(tenantId: "{{TENANT_ID}}", location: "{{LOCATION}}") func auth() -> Auth { - return Auth.auth(app: app) + return Auth.auth(app: app, tenantConfig: tenantConfig) } private init() { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index 5e9f8af3cf0..d4404e493bc 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -53,6 +53,7 @@ enum AuthMenu: String { case phoneEnroll case totpEnroll case multifactorUnenroll + case exchangeToken // More intuitively named getter for `rawValue`. var id: String { rawValue } @@ -139,6 +140,9 @@ enum AuthMenu: String { return "TOTP Enroll" case .multifactorUnenroll: return "Multifactor unenroll" + // R-GCIP Exchange Token + case .exchangeToken: + return "Exchange Token" } } @@ -220,6 +224,8 @@ enum AuthMenu: String { self = .totpEnroll case "Multifactor unenroll": self = .multifactorUnenroll + case "Exchange Token": + self = .exchangeToken default: return nil } @@ -354,9 +360,17 @@ class AuthMenuData: DataSourceProvidable { return Section(headerDescription: header, items: items) } + static var exchangeTokenSection: Section { + let header = "Exchange Token [Regionalized Auth]" + let items: [Item] = [ + Item(title: AuthMenu.exchangeToken.name), + ] + return Section(headerDescription: header, items: items) + } + static let sections: [Section] = [settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection, - customAuthDomainSection, appSection, oobSection, multifactorSection] + customAuthDomainSection, appSection, oobSection, multifactorSection, exchangeTokenSection] static var authLinkSections: [Section] { let allItems = [providerSection, emailPasswordSection, otherSection].flatMap { $0.items } diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index 240346b6975..d858a2c15bc 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -191,6 +191,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .multifactorUnenroll: mfaUnenroll() + + case .exchangeToken: + callExchangeToken() } } @@ -1085,4 +1088,110 @@ extension AuthViewController: ASAuthorizationControllerDelegate, func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return view.window! } + + /// Orchestrates the UI flow to demonstrate the OIDC token exchange feature. + /// + /// This function sequentially prompts the user for the necessary inputs (idpConfigID and custom + /// token) using async/await with UIAlerts. If both inputs are provided, + /// it calls the Auth.exchangeToken API and displays the result to the user. + private func callExchangeToken() { + Task { + do { + // 1. Prompt for the IDP Config ID and await user input. + guard let idpConfigId = await showTextInputPrompt(with: "Enter IDP Config ID:") else { + print("Token exchange cancelled: IDP Config ID was not provided.") + // Present an alert on the main thread to indicate cancellation. + DispatchQueue.main.async { + let alert = UIAlertController(title: "Cancelled", + message: "An IDP Config ID is required to proceed.", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } + return + } + + // 2. Prompt for the custom OIDC token and await user input. + guard let idToken = await showTextInputPrompt(with: "Enter OIDC Token:") else { + print("Token exchange cancelled: OIDC Token was not provided.") + // Present an alert on the main thread to indicate cancellation. + DispatchQueue.main.async { + let alert = UIAlertController(title: "Cancelled", + message: "An OIDC Token is required to proceed.", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } + return + } + + // 3. With both inputs, call the exchangeToken API. + // The `auth()` instance is pre-configured with a regional tenant in AppManager. + print("Attempting to exchange token...") + let result = try await AppManager.shared.auth().exchangeToken( + idToken: idToken, + idpConfigId: idpConfigId, + useStaging: true + ) + + // 4. Handle the success case by presenting an alert on the main thread. + print("Token exchange successful. Access Token: \(result.token)") + DispatchQueue.main.async { + let fullToken = result.token + let truncatedToken = self.truncateString(fullToken, maxLength: 20) + let message = "Firebase Access Token:\n\(truncatedToken)" + let alert = UIAlertController( + title: "Token Exchange Succeeded", + message: message, + preferredStyle: .alert + ) + // Action to copy the token + let copyAction = UIAlertAction(title: "Copy Token", style: .default) { _ in + UIPasteboard.general.string = fullToken + // Show a brief confirmation + self.showCopyConfirmation() + } + alert.addAction(copyAction) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } + + } catch { + // 5. Handle any errors during the process by presenting an alert on the main thread. + print("Failed to exchange token: \(error)") + DispatchQueue.main.async { + let alert = UIAlertController( + title: "Token Exchange Error", + message: error.localizedDescription, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } + } + } + } + + // Helper function to truncate strings + private func truncateString(_ string: String, maxLength: Int) -> String { + if string.count > maxLength { + return String(string.prefix(maxLength)) + "..." + } else { + return string + } + } + + // Helper function to show copy confirmation + private func showCopyConfirmation() { + let confirmationAlert = UIAlertController( + title: "Copied!", + message: "Token copied to clipboard.", + preferredStyle: .alert + ) + present(confirmationAlert, animated: true) + // Automatically dismiss the confirmation after a short delay + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + confirmationAlert.dismiss(animated: true, completion: nil) + } + } }