diff --git a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift index f9cb95190d3..427834e9ba5 100644 --- a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift +++ b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift @@ -42,6 +42,9 @@ import Foundation /// The Firebase Dynamic Link domain used for out of band code flow. @objc open var dynamicLinkDomain: String? + /// The out of band custom domain for handling code in app. + @objc open var linkDomain: String? + /// Sets the iOS bundle ID. @objc override public init() { iOSBundleID = Bundle.main.bundleIdentifier diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift index 7c1fc262c0b..2dfdf433153 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift @@ -78,6 +78,9 @@ private let kCanHandleCodeInAppKey = "canHandleCodeInApp" /// The key for the "dynamic link domain" value in the request. private let kDynamicLinkDomainKey = "dynamicLinkDomain" +/// The key for the "link domain" value in the request. +private let kLinkDomainKey = "linkDomain" + /// The value for the "PASSWORD_RESET" request type. private let kPasswordResetRequestTypeValue = "PASSWORD_RESET" @@ -140,6 +143,9 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { /// The Firebase Dynamic Link domain used for out of band code flow. private(set) var dynamicLinkDomain: String? + /// The Firebase Hosting domain used for out of band code flow. + private(set) var linkDomain: String? + /// Response to the captcha. var captchaResponse: String? @@ -172,6 +178,7 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { androidInstallApp = actionCodeSettings?.androidInstallIfNotAvailable ?? false handleCodeInApp = actionCodeSettings?.handleCodeInApp ?? false dynamicLinkDomain = actionCodeSettings?.dynamicLinkDomain + linkDomain = actionCodeSettings?.linkDomain super.init( endpoint: kGetOobConfirmationCodeEndpoint, @@ -274,6 +281,9 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { if let dynamicLinkDomain { body[kDynamicLinkDomainKey] = dynamicLinkDomain } + if let linkDomain { + body[kLinkDomainKey] = linkDomain + } if let captchaResponse { body[kCaptchaResponseKey] = captchaResponse } diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index f2f4df7c75c..87465c80939 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -43,6 +43,7 @@ enum AuthMenu: String { case deleteApp case actionType case continueURL + case linkDomain case requestVerifyEmail case requestPasswordReset case resetPassword @@ -117,6 +118,8 @@ enum AuthMenu: String { return "Action Type" case .continueURL: return "Continue URL" + case .linkDomain: + return "Link Domain" case .requestVerifyEmail: return "Request Verify Email" case .requestPasswordReset: @@ -197,6 +200,8 @@ enum AuthMenu: String { self = .actionType case "Continue URL": self = .continueURL + case "Link Domain": + self = .linkDomain case "Request Verify Email": self = .requestVerifyEmail case "Request Password Reset": @@ -328,6 +333,7 @@ class AuthMenuData: DataSourceProvidable { let items: [Item] = [ Item(title: AuthMenu.actionType.name, detailTitle: ActionCodeRequestType.inApp.name), Item(title: AuthMenu.continueURL.name, detailTitle: "--", isEditable: true), + Item(title: AuthMenu.linkDomain.name, detailTitle: "--", isEditable: true), Item(title: AuthMenu.requestVerifyEmail.name), Item(title: AuthMenu.requestPasswordReset.name), Item(title: AuthMenu.resetPassword.name), diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift index 2ecae6d8a5f..24e4199b3f2 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift @@ -317,6 +317,9 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate /// Similar to in `PasswordlessViewController`, enter the authorized domain. /// Please refer to this Quickstart's README for more information. private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN" + + /// This is the replacement for customized dynamic link domain. + private let customDomain: String = "ENTER AUTHORIZED HOSTING DOMAIN" /// Maintain a reference to the email entered for linking user to Passwordless. private var email: String? @@ -343,6 +346,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate // The sign-in operation must be completed in the app. actionCodeSettings.handleCodeInApp = true actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!) + actionCodeSettings.linkDomain = customDomain AppManager.shared.auth() .sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index fdde2e0757d..71a7b6a8654 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -39,6 +39,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = [] var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = [] var actionCodeContinueURL: URL? + var actionCodeLinkDomain: String? var actionCodeRequestType: ActionCodeRequestType = .inApp let spinner = UIActivityIndicatorView(style: .medium) @@ -69,6 +70,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { let settings = ActionCodeSettings() settings.url = actionCodeContinueURL settings.handleCodeInApp = (actionCodeRequestType == .inApp) + settings.linkDomain = actionCodeLinkDomain return settings } @@ -156,6 +158,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .continueURL: changeActionCodeContinueURL(at: indexPath) + case .linkDomain: + changeActionCodeLinkDomain(at: indexPath) + case .requestVerifyEmail: requestVerifyEmail() @@ -552,7 +557,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { private func changeActionCodeContinueURL(at indexPath: IndexPath) { showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in self.actionCodeContinueURL = URL(string: newContinueURL) - print("Successfully set Continue URL to: \(newContinueURL)") + print("Successfully set Continue URL to: \(newContinueURL)") self.dataSourceProvider.updateItem( at: indexPath, item: Item( @@ -565,6 +570,22 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { }) } + private func changeActionCodeLinkDomain(at indexPath: IndexPath) { + showTextInputPrompt(with: "Link Domain:", completion: { newLinkDomain in + self.actionCodeLinkDomain = newLinkDomain + print("Successfully set Link Domain to: \(newLinkDomain)") + self.dataSourceProvider.updateItem( + at: indexPath, + item: Item( + title: AuthMenu.linkDomain.name, + detailTitle: self.actionCodeLinkDomain, + isEditable: true + ) + ) + self.tableView.reloadData() + }) + } + private func requestVerifyEmail() { showSpinner() let completionHandler: ((any Error)?) -> Void = { [weak self] error in diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift index d710f323a6a..ba67023a2d8 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift @@ -32,6 +32,7 @@ class PasswordlessViewController: OtherAuthViewController { // MARK: - Firebase 🔥 private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN" + private let customDomain: String = "ENTER AUTHORIZED HOSTING DOMAIN" private func sendSignInLink(to email: String) { let actionCodeSettings = ActionCodeSettings() @@ -42,6 +43,7 @@ class PasswordlessViewController: OtherAuthViewController { // The sign-in operation must be completed in the app. actionCodeSettings.handleCodeInApp = true actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!) + actionCodeSettings.linkDomain = customDomain AppManager.shared.auth() .sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index ac8d7953ae3..791b5c7a2b5 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -226,6 +226,35 @@ class AuthenticationExampleUITests: XCTestCase { removeUIInterruptionMonitor(interruptionMonitor) } + func testEmailLinkSentSuccessfully() { + app.staticTexts["Email Link/Passwordless"].tap() + + let testEmail = "test@test.com" + app.textFields["Enter Authentication Email"].tap() + app.textFields["Enter Authentication Email"].typeText(testEmail) + app.buttons["return"].tap() // Dismiss keyboard + app.buttons["Send Sign In Link"].tap() + + // Wait for the error message to appear (if there is an error) + let errorAlert = app.alerts.staticTexts["Error"] + let errorExists = errorAlert.waitForExistence(timeout: 5.0) + + app.swipeDown(velocity: .fast) + + // Assert that there is no error message (success case) + // The email sign in link is sent successfully if no error message appears + XCTAssertFalse(errorExists, "Error") + + // Go back and check that there is no user that is signed in + app.tabBars.firstMatch.buttons.element(boundBy: 1).tap() + wait(forElement: app.navigationBars["User"], timeout: 5.0) + XCTAssertEqual( + app.cells.count, + 0, + "The user shouldn't be signed in and the user view should have no cells." + ) + } + // MARK: - Private Helpers private func signOut() { diff --git a/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift b/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift index b9fe798f57d..5f92932c8bb 100644 --- a/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift +++ b/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift @@ -34,6 +34,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests { private let kAndroidMinimumVersionKey = "androidMinimumVersion" private let kCanHandleCodeInAppKey = "canHandleCodeInApp" private let kDynamicLinkDomainKey = "dynamicLinkDomain" + private let kLinkDomainKey = "linkDomain" private let kExpectedAPIURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=APIKey" private let kOOBCodeKey = "oobCode" @@ -66,6 +67,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests { XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true) XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true) XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain) + XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain) } } @@ -110,6 +112,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests { XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true) XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true) XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain) + XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain) XCTAssertEqual(decodedRequest[kCaptchaResponseKey] as? String, kTestCaptchaResponse) XCTAssertEqual(decodedRequest[kClientTypeKey] as? String, kTestClientType) XCTAssertEqual(decodedRequest[kRecaptchaVersionKey] as? String, kTestRecaptchaVersion) diff --git a/FirebaseAuth/Tests/Unit/RPCBaseTests.swift b/FirebaseAuth/Tests/Unit/RPCBaseTests.swift index 7e7b865287c..b918e28eda7 100644 --- a/FirebaseAuth/Tests/Unit/RPCBaseTests.swift +++ b/FirebaseAuth/Tests/Unit/RPCBaseTests.swift @@ -38,6 +38,7 @@ class RPCBaseTests: XCTestCase { let kAndroidPackageName = "androidpackagename" let kAndroidMinimumVersion = "3.0" let kDynamicLinkDomain = "test.page.link" + let kLinkDomain = "link.firebaseapp.com" let kTestPhotoURL = "https://host.domain/image" let kCreationDateTimeIntervalInSeconds = 1_505_858_500.0 let kLastSignInDateTimeIntervalInSeconds = 1_505_858_583.0 @@ -304,6 +305,7 @@ class RPCBaseTests: XCTestCase { settings.handleCodeInApp = true settings.url = URL(string: kContinueURL) settings.dynamicLinkDomain = kDynamicLinkDomain + settings.linkDomain = kLinkDomain return settings }