Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add linkDomain support for ActionCodeSettting. This will generate Hosting-formatted email link. #13991

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import Foundation
/// The Firebase Dynamic Link domain used for out of band code flow.
@objc open var dynamicLinkDomain: String?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be marked deprecated? Or wait until next release?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I mark this as deprecated, our test failed due to build warnings. Any suggestions? I still want to keep the dynamicLinkDomain tests because both solutions will be available until FDL sunset.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can temporarily add --allow-warnings with a comment to the pod lib lint test invocations


/// 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
Expand Down
2 changes: 2 additions & 0 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,8 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
.missingAppCredential(message: serverDetailErrorMessage)
case "INVALID_CODE": return AuthErrorUtils
.invalidVerificationCodeError(message: serverDetailErrorMessage)
case "INVALID_HOSTING_LINK_DOMAIN": return AuthErrorUtils
.invalidHostingLinkDomainError(message: serverDetailErrorMessage)
case "INVALID_SESSION_INFO": return AuthErrorUtils
.invalidVerificationIDError(message: serverDetailErrorMessage)
case "SESSION_EXPIRED": return AuthErrorUtils
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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?

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ class AuthErrorUtils {
error(code: .invalidDynamicLinkDomain, message: message)
}

static func invalidHostingLinkDomainError(message: String?) -> Error {
error(code: .invalidHostingLinkDomain, message: message)
}

static func missingOrInvalidNonceError(message: String?) -> Error {
error(code: .missingOrInvalidNonce, message: message)
}
Expand Down
10 changes: 10 additions & 0 deletions FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ import Foundation
/// unauthorized for the current project.
case invalidDynamicLinkDomain = 17074

/// Indicates that the provided Firebase Hosting Link domain is not owned by the current project.
case invalidHostingLinkDomain = 17214

/// Indicates that the credential is rejected because it's malformed or mismatching.
case rejectedCredential = 17075

Expand Down Expand Up @@ -468,6 +471,8 @@ import Foundation
return kErrorInvalidProviderID
case .invalidDynamicLinkDomain:
return kErrorInvalidDynamicLinkDomain
case .invalidHostingLinkDomain:
return kErrorInvalidHostingLinkDomain
case .webInternalError:
return kErrorWebInternalError
case .webSignInUserInteractionFailure:
Expand Down Expand Up @@ -661,6 +666,8 @@ import Foundation
return "ERROR_INVALID_PROVIDER_ID"
case .invalidDynamicLinkDomain:
return "ERROR_INVALID_DYNAMIC_LINK_DOMAIN"
case .invalidHostingLinkDomain:
return "ERROR_INVALID_HOSTING_LINK_DOMAIN"
case .webInternalError:
return "ERROR_WEB_INTERNAL_ERROR"
case .webSignInUserInteractionFailure:
Expand Down Expand Up @@ -905,6 +912,9 @@ private let kErrorInvalidProviderID =
private let kErrorInvalidDynamicLinkDomain =
"The Firebase Dynamic Link domain used is either not configured or is unauthorized for the current project."

private let kErrorInvalidHostingLinkDomain =
"The provided hosting link domain is not configured in Firebase Hosting or is not owned by the current project."

private let kErrorInternalError =
"An internal error has occurred, print and inspect the error details for more information."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum AuthMenu: String {
case deleteApp
case actionType
case continueURL
case linkDomain
case requestVerifyEmail
case requestPasswordReset
case resetPassword
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,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?

Expand All @@ -380,6 +383,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -69,6 +70,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
let settings = ActionCodeSettings()
settings.url = actionCodeContinueURL
settings.handleCodeInApp = (actionCodeRequestType == .inApp)
settings.linkDomain = actionCodeLinkDomain
return settings
}

Expand Down Expand Up @@ -156,6 +158,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
case .continueURL:
changeActionCodeContinueURL(at: indexPath)

case .linkDomain:
changeActionCodeLinkDomain(at: indexPath)

case .requestVerifyEmail:
requestVerifyEmail()

Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ class PasswordlessViewController: OtherAuthViewController {

// MARK: - Firebase 🔥

private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN"
private let authorizedDomain: String =
"fir-ios-auth-sample.firebaseapp.com" // Enter AUTHORIZED_DOMAIN
private let customDomain: String =
"firebaseiosauthsample.testdomaindonotuse.com" // Enter AUTHORIZED_HOSTING_DOMAIN

private func sendSignInLink(to email: String) {
let actionCodeSettings = ActionCodeSettings()
Expand All @@ -42,6 +45,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,89 @@ class AuthenticationExampleUITests: XCTestCase {
removeUIInterruptionMonitor(interruptionMonitor)
}

func testEmailLinkSentSuccessfully() {
app.staticTexts["Email Link/Passwordless"].tap()

let testEmail = "[email protected]"
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."
)
}

func testResetPasswordLinkCustomDomain() {
// assuming action type is in-app + continue URL everytime the app launches

// set Authorized Domain as Continue URL
let testContinueURL = "fir-ios-auth-sample.firebaseapp.com"
app.staticTexts["Continue URL"].tap()
app.alerts.textFields.element.typeText(testContinueURL)
app.buttons["Save"].tap()

// set Custom Hosting Domain as Link Domain
let testLinkDomain = "http://firebaseiosauthsample.testdomaindonotuse.com"
app.staticTexts["Link Domain"].tap()
app.alerts.textFields.element.typeText(testLinkDomain)
app.buttons["Save"].tap()

app.staticTexts["Request Password Reset"].tap()
let testEmail = "[email protected]"
app.alerts.textFields.element.typeText(testEmail)
app.buttons["Save"].tap()

// 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."
)
}

func testResetPasswordLinkDefaultDomain() {
// assuming action type is in-app + continue URL everytime the app launches

// set Authorized Domain as Continue URL
let testContinueURL = "fir-ios-auth-sample.firebaseapp.com"
app.staticTexts["Continue URL"].tap()
app.alerts.textFields.element.typeText(testContinueURL)
app.buttons["Save"].tap()

app.staticTexts["Request Password Reset"].tap()
let testEmail = "[email protected]"
app.alerts.textFields.element.typeText(testEmail)
app.buttons["Save"].tap()

// 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() {
Expand Down
3 changes: 3 additions & 0 deletions FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions FirebaseAuth/Tests/Unit/ObjCAPITests.m
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ - (void)FIRActionCodeSettings_h {
s = [codeSettings androidPackageName];
s = [codeSettings androidMinimumVersion];
s = [codeSettings dynamicLinkDomain];
s = [codeSettings linkDomain];
}

- (void)FIRAuthAdditionalUserInfo_h:(FIRAdditionalUserInfo *)additionalUserInfo {
Expand Down Expand Up @@ -280,6 +281,7 @@ - (void)FIRAuthErrors_h {
c = FIRAuthErrorCodeTenantIDMismatch;
c = FIRAuthErrorCodeUnsupportedTenantOperation;
c = FIRAuthErrorCodeInvalidDynamicLinkDomain;
c = FIRAuthErrorCodeInvalidHostingLinkDomain;
c = FIRAuthErrorCodeRejectedCredential;
c = FIRAuthErrorCodeGameKitNotLinked;
c = FIRAuthErrorCodeSecondFactorRequired;
Expand Down
2 changes: 2 additions & 0 deletions FirebaseAuth/Tests/Unit/RPCBaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -304,6 +305,7 @@ class RPCBaseTests: XCTestCase {
settings.handleCodeInApp = true
settings.url = URL(string: kContinueURL)
settings.dynamicLinkDomain = kDynamicLinkDomain
settings.linkDomain = kLinkDomain
return settings
}

Expand Down
4 changes: 3 additions & 1 deletion FirebaseAuth/Tests/Unit/SwiftAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class AuthAPI_hOnlyTests: XCTestCase {
let _: String = codeSettings.iOSBundleID,
let _: String = codeSettings.androidPackageName,
let _: String = codeSettings.androidMinimumVersion,
let _: String = codeSettings.dynamicLinkDomain {}
let _: String = codeSettings.dynamicLinkDomain,
let _: String = codeSettings.linkDomain {}
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
Expand Down Expand Up @@ -276,6 +277,7 @@ class AuthAPI_hOnlyTests: XCTestCase {
_ = AuthErrorCode.tenantIDMismatch
_ = AuthErrorCode.unsupportedTenantOperation
_ = AuthErrorCode.invalidDynamicLinkDomain
_ = AuthErrorCode.invalidHostingLinkDomain
_ = AuthErrorCode.rejectedCredential
_ = AuthErrorCode.gameKitNotLinked
_ = AuthErrorCode.secondFactorRequired
Expand Down
Loading