Skip to content

Commit

Permalink
Add ActionCodeSettings.linkDomain and deprecate ActionCodeSettings.dy…
Browse files Browse the repository at this point in the history
…namicLinkDomain (#8738)

* Add linkDomain field to ActionCodeSettings (#8428)

* Add linkDomain field to ActionCodeSettings

* Update API reports

* Update error message for ERROR_INVALID_HOSTING_LINK_DOMAIN to include that default hosting domains cannot be used.

* Use constants for test values

---------

Co-authored-by: NhienLam <[email protected]>

* FDL Deprecation & Hosting link Integration Test (#8603)

* Implement Integration test for passwordless email sign-in via firebase-hosting links.

* Add license to new test file created' did not match any files

* Remove unwanted _

* Add support for setting custom hosting link domain in test app (#8614)

* Add afterEach method and apply formatting to the hosting links integration test (#8615)

* Mark ActionCodeSettings.dynamicLinkDomain as deprecated

* Add changeset

* Update API reports

* Address review comments

* Fix changeset

* Update refdocs and address review comments

* Update deprecated comment

* Add FDL deprecation FAQ link

* Fix linkDomain link in refdocs

---------

Co-authored-by: NhienLam <[email protected]>
Co-authored-by: mansisampat <[email protected]>
  • Loading branch information
3 people authored Feb 4, 2025
1 parent 2f92a74 commit 9d88e3a
Show file tree
Hide file tree
Showing 18 changed files with 232 additions and 37 deletions.
7 changes: 7 additions & 0 deletions .changeset/polite-lies-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'firebase': minor
'@firebase/auth-types': minor
'@firebase/auth': minor
---

Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain that is used in mobile out-of-band email action flows. Also, deprecated `ActionCodeSettings.dynamicLinkDomain`.
3 changes: 3 additions & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ export interface ActionCodeSettings {
minimumVersion?: string;
packageName: string;
};
// @deprecated
dynamicLinkDomain?: string;
handleCodeInApp?: boolean;
iOS?: {
bundleId: string;
};
linkDomain?: string;
url: string;
}

Expand Down Expand Up @@ -236,6 +238,7 @@ export const AuthErrorCodes: {
readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version";
readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version";
readonly INVALID_REQ_TYPE: "auth/invalid-req-type";
readonly INVALID_HOSTING_LINK_DOMAIN: "auth/invalid-hosting-link-domain";
};

// @public
Expand Down
22 changes: 18 additions & 4 deletions docs-devsite/auth.actioncodesettings.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ export interface ActionCodeSettings
| [dynamicLinkDomain](./auth.actioncodesettings.md#actioncodesettingsdynamiclinkdomain) | string | When multiple custom dynamic link domains are defined for a project, specify which one to use when the link is to be opened via a specified mobile app (for example, <code>example.page.link</code>). |
| [handleCodeInApp](./auth.actioncodesettings.md#actioncodesettingshandlecodeinapp) | boolean | When set to true, the action code link will be be sent as a Universal Link or Android App Link and will be opened by the app if installed. |
| [iOS](./auth.actioncodesettings.md#actioncodesettingsios) | { bundleId: string; } | Sets the iOS bundle ID. |
| [linkDomain](./auth.actioncodesettings.md#actioncodesettingslinkdomain) | string | The optional custom Firebase Hosting domain to use when the link is to be opened via a specified mobile app. The domain must be configured in Firebase Hosting and owned by the project. This cannot be a default Hosting domain (<code>web.app</code> or <code>firebaseapp.com</code>). |
| [url](./auth.actioncodesettings.md#actioncodesettingsurl) | string | Sets the link continue/state URL. |

## ActionCodeSettings.android

Sets the Android package name.

This will try to open the link in an android app if it is installed. If `installApp` is passed, it specifies whether to install the Android app if the device supports it and the app is not already installed. If this field is provided without a `packageName`<!-- -->, an error is thrown explaining that the `packageName` must be provided in conjunction with this field. If `minimumVersion` is specified, and an older version of the app is installed, the user is taken to the Play Store to upgrade the app.
This will try to open the link in an Android app if it is installed.

<b>Signature:</b>

Expand All @@ -46,6 +47,11 @@ android?: {

## ActionCodeSettings.dynamicLinkDomain

> Warning: This API is now obsolete.
>
> Firebase Dynamic Links is deprecated and will be shut down as early as August 2025. Instead, use [ActionCodeSettings.linkDomain](./auth.actioncodesettings.md#actioncodesettingslinkdomain) to set a custom domain for mobile links. Learn more in the [Dynamic Links deprecation FAQ](https://firebase.google.com/support/dynamic-links-faq)<!-- -->.
>

When multiple custom dynamic link domains are defined for a project, specify which one to use when the link is to be opened via a specified mobile app (for example, `example.page.link`<!-- -->).

<b>Signature:</b>
Expand All @@ -72,8 +78,6 @@ Sets the iOS bundle ID.

This will try to open the link in an iOS app if it is installed.

App installation is not supported for iOS.

<b>Signature:</b>

```typescript
Expand All @@ -82,11 +86,21 @@ iOS?: {
};
```

## ActionCodeSettings.linkDomain

The optional custom Firebase Hosting domain to use when the link is to be opened via a specified mobile app. The domain must be configured in Firebase Hosting and owned by the project. This cannot be a default Hosting domain (`web.app` or `firebaseapp.com`<!-- -->).

<b>Signature:</b>

```typescript
linkDomain?: string;
```

## ActionCodeSettings.url

Sets the link continue/state URL.

This has different meanings in different contexts: - When the link is handled in the web action widgets, this is the deep link in the `continueUrl` query parameter. - When the link is handled in the app directly, this is the `continueUrl` query parameter in the deep link of the Dynamic Link.
This has different meanings in different contexts: - When the link is handled in the web action widgets, this is the deep link in the `continueUrl` query parameter. - When the link is handled in the app directly, this is the `continueUrl` query parameter in the deep link of the Dynamic Link or Hosting link.

<b>Signature:</b>

Expand Down
1 change: 1 addition & 0 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -1956,6 +1956,7 @@ AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY: {
readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version";
readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version";
readonly INVALID_REQ_TYPE: "auth/invalid-req-type";
readonly INVALID_HOSTING_LINK_DOMAIN: "auth/invalid-hosting-link-domain";
}
```

Expand Down
1 change: 1 addition & 0 deletions packages/auth-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export type ActionCodeSettings = {
iOS?: { bundleId: string };
url: string;
dynamicLinkDomain?: string;
linkDomain?: string;
};

export type AdditionalUserInfo = {
Expand Down
5 changes: 5 additions & 0 deletions packages/auth/demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,11 @@
<input type="text" class="form-control" id="ibi" placeholder="iOS Bundle ID"/>
</div>
</form>
<div class="group">Mobile link</div>
<div class="form-group">
<input type="text" class="form-control" id="hostingLinkDomain"
placeholder="Custom Hosting Link Domain"/>
</div>
<form class="form form-bordered no-submit">
<div class="btn-group radio-block" id="handle-in-app-selection"
data-toggle="buttons">
Expand Down
7 changes: 6 additions & 1 deletion packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ import {
signInWithCredential,
signInWithCustomToken,
signInWithEmailAndPassword,
signInWithEmailLink,
TotpMultiFactorGenerator,
TotpSecret,
unlink,
updateEmail,
updatePassword,
Expand Down Expand Up @@ -995,6 +995,7 @@ function getActionCodeSettings() {
const installApp = $('input[name=install-app]:checked').val() === 'Yes';
const handleCodeInApp =
$('input[name=handle-in-app]:checked').val() === 'Yes';
const hostingLinkDomain = $('#hostingLinkDomain').val();
if (url || apn || ibi) {
actionCodeSettings['url'] = url;
if (apn) {
Expand All @@ -1010,6 +1011,9 @@ function getActionCodeSettings() {
};
}
actionCodeSettings['handleCodeInApp'] = handleCodeInApp;
if (hostingLinkDomain) {
actionCodeSettings['linkDomain'] = hostingLinkDomain;
}
}
return actionCodeSettings;
}
Expand All @@ -1020,6 +1024,7 @@ function onActionCodeSettingsReset() {
$('#apn').val('');
$('#amv').val('');
$('#ibi').val('');
$('#hostingLinkDomain').val('');
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ function getTestFiles(argv) {
return [
'test/integration/flows/totp.test.ts',
'test/integration/flows/password_policy.test.ts',
'test/integration/flows/recaptcha_enterprise.test.ts'
'test/integration/flows/recaptcha_enterprise.test.ts',
'test/integration/flows/hosting_link.test.ts'
];
}
return argv.local
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/api/authentication/email_and_password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface GetOobCodeRequest {
dynamicLinkDomain?: string;
tenantId?: string;
targetProjectid?: string;
linkDomain?: string;
}

export interface VerifyEmailRequest extends GetOobCodeRequest {
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export const enum ServerError {
MISSING_RECAPTCHA_VERSION = 'MISSING_RECAPTCHA_VERSION',
INVALID_RECAPTCHA_VERSION = 'INVALID_RECAPTCHA_VERSION',
INVALID_REQ_TYPE = 'INVALID_REQ_TYPE',
PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'PASSWORD_DOES_NOT_MEET_REQUIREMENTS'
PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'PASSWORD_DOES_NOT_MEET_REQUIREMENTS',
INVALID_HOSTING_LINK_DOMAIN = 'INVALID_HOSTING_LINK_DOMAIN'
}

/**
Expand Down
11 changes: 8 additions & 3 deletions packages/auth/src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ export const enum AuthErrorCode {
INVALID_RECAPTCHA_VERSION = 'invalid-recaptcha-version',
INVALID_REQ_TYPE = 'invalid-req-type',
UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION = 'unsupported-password-policy-schema-version',
PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'password-does-not-meet-requirements'
PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'password-does-not-meet-requirements',
INVALID_HOSTING_LINK_DOMAIN = 'invalid-hosting-link-domain'
}

function _debugErrorMap(): ErrorMap<AuthErrorCode> {
Expand Down Expand Up @@ -387,7 +388,10 @@ function _debugErrorMap(): ErrorMap<AuthErrorCode> {
[AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION]:
'The password policy received from the backend uses a schema version that is not supported by this version of the Firebase SDK.',
[AuthErrorCode.PASSWORD_DOES_NOT_MEET_REQUIREMENTS]:
'The password does not meet the requirements.'
'The password does not meet the requirements.',
[AuthErrorCode.INVALID_HOSTING_LINK_DOMAIN]:
'The provided Hosting link domain is not configured in Firebase Hosting or is not owned by ' +
'the current project. This cannot be a default Hosting domain (`web.app` or `firebaseapp.com`).'
};
}

Expand Down Expand Up @@ -598,5 +602,6 @@ export const AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY = {
MISSING_CLIENT_TYPE: 'auth/missing-client-type',
MISSING_RECAPTCHA_VERSION: 'auth/missing-recaptcha-version',
INVALID_RECAPTCHA_VERSION: 'auth/invalid-recaptcha-version',
INVALID_REQ_TYPE: 'auth/invalid-req-type'
INVALID_REQ_TYPE: 'auth/invalid-req-type',
INVALID_HOSTING_LINK_DOMAIN: 'auth/invalid-hosting-link-domain'
} as const;
37 changes: 27 additions & 10 deletions packages/auth/src/core/strategies/action_code_settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ describe('core/strategies/action_code_settings', () => {
let auth: TestAuth;
const request: GetOobCodeRequest = {};

const TEST_BUNDLE_ID = 'my-bundle';
const TEST_FDL_DOMAIN = 'fdl-domain';
const TEST_URL = 'my-url';

beforeEach(async () => {
auth = await testAuth();
});
Expand All @@ -35,10 +39,10 @@ describe('core/strategies/action_code_settings', () => {
_setActionCodeSettingsOnRequest(auth, request, {
handleCodeInApp: true,
iOS: {
bundleId: 'my-bundle'
bundleId: TEST_BUNDLE_ID
},
url: '',
dynamicLinkDomain: 'fdl-domain'
dynamicLinkDomain: TEST_FDL_DOMAIN
})
).to.throw(FirebaseError, '(auth/invalid-continue-uri)');
});
Expand All @@ -48,9 +52,9 @@ describe('core/strategies/action_code_settings', () => {
_setActionCodeSettingsOnRequest(auth, request, {
handleCodeInApp: true,
iOS: {
bundleId: 'my-´bundle'
bundleId: TEST_BUNDLE_ID
},
url: 'my-url'
url: TEST_URL
})
).to.not.throw();
});
Expand All @@ -60,23 +64,36 @@ describe('core/strategies/action_code_settings', () => {
_setActionCodeSettingsOnRequest(auth, request, {
handleCodeInApp: true,
iOS: {
bundleId: 'my-´bundle'
bundleId: TEST_BUNDLE_ID
},
url: 'my-url',
url: TEST_URL,
dynamicLinkDomain: ''
})
).to.throw(FirebaseError, '(auth/invalid-dynamic-link-domain)');
});

it('should require a non empty Hosting link URL', () => {
expect(() =>
_setActionCodeSettingsOnRequest(auth, request, {
handleCodeInApp: true,
iOS: {
bundleId: TEST_BUNDLE_ID
},
url: TEST_URL,
linkDomain: ''
})
).to.throw(FirebaseError, '(auth/invalid-hosting-link-domain)');
});

it('should require a non-empty bundle ID', () => {
expect(() =>
_setActionCodeSettingsOnRequest(auth, request, {
handleCodeInApp: true,
iOS: {
bundleId: ''
},
url: 'my-url',
dynamicLinkDomain: 'fdl-domain'
url: TEST_URL,
dynamicLinkDomain: TEST_FDL_DOMAIN
})
).to.throw(FirebaseError, '(auth/missing-ios-bundle-id)');
});
Expand All @@ -88,8 +105,8 @@ describe('core/strategies/action_code_settings', () => {
android: {
packageName: ''
},
url: 'my-url',
dynamicLinkDomain: 'fdl-domain'
url: TEST_URL,
dynamicLinkDomain: TEST_FDL_DOMAIN
})
).to.throw(FirebaseError, '(auth/missing-android-pkg-name)');
});
Expand Down
7 changes: 7 additions & 0 deletions packages/auth/src/core/strategies/action_code_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,16 @@ export function _setActionCodeSettingsOnRequest(
auth,
AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN
);
_assert(
typeof actionCodeSettings.linkDomain === 'undefined' ||
actionCodeSettings.linkDomain.length > 0,
auth,
AuthErrorCode.INVALID_HOSTING_LINK_DOMAIN
);

request.continueUrl = actionCodeSettings.url;
request.dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain;
request.linkDomain = actionCodeSettings.linkDomain;
request.canHandleCodeInApp = actionCodeSettings.handleCodeInApp;

if (actionCodeSettings.iOS) {
Expand Down
16 changes: 12 additions & 4 deletions packages/auth/src/core/strategies/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,16 @@ describe('core/strategies/sendEmailVerification', () => {
bundleId: 'my-bundle'
},
url: 'my-url',
dynamicLinkDomain: 'fdl-domain'
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain'
});

expect(mock.calls[0].request).to.eql({
requestType: ActionCodeOperation.VERIFY_EMAIL,
idToken,
continueUrl: 'my-url',
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain',
canHandleCodeInApp: true,
iOSBundleId: 'my-bundle'
});
Expand All @@ -190,13 +192,15 @@ describe('core/strategies/sendEmailVerification', () => {
packageName: 'my-package'
},
url: 'my-url',
dynamicLinkDomain: 'fdl-domain'
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain'
});
expect(mock.calls[0].request).to.eql({
requestType: ActionCodeOperation.VERIFY_EMAIL,
idToken,
continueUrl: 'my-url',
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain',
canHandleCodeInApp: true,
androidInstallApp: false,
androidMinimumVersionCode: 'my-version',
Expand Down Expand Up @@ -270,7 +274,8 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => {
bundleId: 'my-bundle'
},
url: 'my-url',
dynamicLinkDomain: 'fdl-domain'
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain'
});

expect(mock.calls[0].request).to.eql({
Expand All @@ -279,6 +284,7 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => {
newEmail,
continueUrl: 'my-url',
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain',
canHandleCodeInApp: true,
iOSBundleId: 'my-bundle'
});
Expand All @@ -299,14 +305,16 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => {
packageName: 'my-package'
},
url: 'my-url',
dynamicLinkDomain: 'fdl-domain'
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain'
});
expect(mock.calls[0].request).to.eql({
requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL,
idToken,
newEmail,
continueUrl: 'my-url',
dynamicLinkDomain: 'fdl-domain',
linkDomain: 'hosting-link-domain',
canHandleCodeInApp: true,
androidInstallApp: false,
androidMinimumVersionCode: 'my-version',
Expand Down
Loading

0 comments on commit 9d88e3a

Please sign in to comment.