diff --git a/.changeset/chatty-coats-sin.md b/.changeset/chatty-coats-sin.md
new file mode 100644
index 0000000000..97f74bee80
--- /dev/null
+++ b/.changeset/chatty-coats-sin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-defaults': patch
+---
+
+Deprecated `createPublicSignInApp`, which has been replaced by the new `appModulePublicSignIn` from `@backstage/plugin-app/alpha` instead.
diff --git a/.changeset/four-spiders-jump.md b/.changeset/four-spiders-jump.md
new file mode 100644
index 0000000000..d5a92b8745
--- /dev/null
+++ b/.changeset/four-spiders-jump.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-app': patch
+---
+
+Added a new module for implementing public sign-in apps, exported as `appModulePublicSignIn` via the `/alpha` sub-path export. This replaces the `createPublicSignInApp` export from `@backstage/frontend-defaults`, which is now deprecated.
diff --git a/packages/app-next/src/index-public-experimental.tsx b/packages/app-next/src/index-public-experimental.tsx
index 34198f9553..ce558ed4f0 100644
--- a/packages/app-next/src/index-public-experimental.tsx
+++ b/packages/app-next/src/index-public-experimental.tsx
@@ -15,9 +15,13 @@
*/
import ReactDOM from 'react-dom/client';
-import { createPublicSignInApp } from '@backstage/frontend-defaults';
+import { createApp } from '@backstage/frontend-defaults';
+import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
+
import '@backstage/ui/css/styles.css';
-const app = createPublicSignInApp();
+const app = createApp({
+ features: [appModulePublicSignIn],
+});
ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot());
diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md
index 37c770074e..171cb40fbe 100644
--- a/packages/frontend-defaults/report.api.md
+++ b/packages/frontend-defaults/report.api.md
@@ -41,7 +41,7 @@ export interface CreateAppOptions {
pluginInfoResolver?: FrontendPluginInfoResolver;
}
-// @public
+// @public @deprecated (undocumented)
export function createPublicSignInApp(options?: CreateAppOptions): {
createRoot(): JSX_2;
};
diff --git a/packages/frontend-defaults/src/createPublicSignInApp.tsx b/packages/frontend-defaults/src/createPublicSignInApp.tsx
index af7107681b..0015d9403f 100644
--- a/packages/frontend-defaults/src/createPublicSignInApp.tsx
+++ b/packages/frontend-defaults/src/createPublicSignInApp.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 The Backstage Authors
+ * Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,94 +14,16 @@
* limitations under the License.
*/
-import {
- coreExtensionData,
- createFrontendModule,
- identityApiRef,
- useApi,
-} from '@backstage/frontend-plugin-api';
-import { useAsync, useMountEffect } from '@react-hookz/web';
+import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
import { CreateAppOptions, createApp } from './createApp';
-import appPlugin from '@backstage/plugin-app';
-
-// This is a copy of the CookieAuthRedirect component from the auth-react
-// plugin, to avoid a dependency on that package. Long-term we want this to be
-// the only implementation and remove the one in auth-react once the old frontend system is gone.
-
-// TODO(Rugvip): Should this be part of the app plugin instead? since it owns the backend part of it.
-
-/** @internal */
-export function InternalCookieAuthRedirect() {
- const identityApi = useApi(identityApiRef);
-
- const [state, actions] = useAsync(async () => {
- const { token } = await identityApi.getCredentials();
- if (!token) {
- throw new Error('Expected Backstage token in sign-in response');
- }
- return token;
- });
-
- useMountEffect(actions.execute);
-
- if (state.status === 'error' && state.error) {
- return <>An error occurred: {state.error.message}>;
- }
-
- if (state.status === 'success' && state.result) {
- return (
-
- );
- }
-
- return null;
-}
/**
- * Creates an app that is suitable for the public sign-in page, for use in the `index-public-experimental.tsx` file.
- *
- * @remarks
- *
- * This app has an override for the `app/layout` extension, which means that
- * most extension typically installed in an app will be ignored. However, you
- * can still for example install API and root element extensions.
- *
- * A typical setup of this app will only install a custom sign-in page.
- *
- * @example
- * ```ts
- * const app = createPublicSignInApp({
- * features: [signInPageModule],
- * });
- * ```
- *
* @public
+ * @deprecated Use {@link @backstage/plugin-app/alpha#appModulePublicSignIn} instead.
*/
export function createPublicSignInApp(options?: CreateAppOptions) {
return createApp({
...options,
- features: [
- ...(options?.features ?? []),
- // This is a rather than app plugin override in order for it to take precedence over any supplied app plugin override
- createFrontendModule({
- pluginId: 'app',
- extensions: [
- appPlugin.getExtension('app/layout').override({
- factory: () => [
- coreExtensionData.reactElement(),
- ],
- }),
- ],
- }),
- ],
+ features: [...(options?.features ?? []), appModulePublicSignIn],
});
}
diff --git a/plugins/app/package.json b/plugins/app/package.json
index 199054f141..9cefaa8609 100644
--- a/plugins/app/package.json
+++ b/plugins/app/package.json
@@ -11,9 +11,7 @@
]
},
"publishConfig": {
- "access": "public",
- "main": "dist/index.esm.js",
- "types": "dist/index.d.ts"
+ "access": "public"
},
"repository": {
"type": "git",
@@ -22,6 +20,21 @@
},
"license": "Apache-2.0",
"sideEffects": false,
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha/index.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha/index.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
@@ -47,12 +60,15 @@
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
+ "@react-hookz/web": "^24.0.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/dev-utils": "workspace:^",
+ "@backstage/frontend-defaults": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
+ "@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
diff --git a/plugins/app/report-alpha.api.md b/plugins/app/report-alpha.api.md
new file mode 100644
index 0000000000..9496430e7b
--- /dev/null
+++ b/plugins/app/report-alpha.api.md
@@ -0,0 +1,12 @@
+## API Report File for "@backstage/plugin-app"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { FrontendModule } from '@backstage/frontend-plugin-api';
+
+// @alpha
+export const appModulePublicSignIn: FrontendModule;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/app/src/alpha/appModulePublicSignIn.test.tsx b/plugins/app/src/alpha/appModulePublicSignIn.test.tsx
new file mode 100644
index 0000000000..14dba9416b
--- /dev/null
+++ b/plugins/app/src/alpha/appModulePublicSignIn.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ SignInPageBlueprint,
+ createFrontendModule,
+} from '@backstage/frontend-plugin-api';
+import { render, screen, waitFor } from '@testing-library/react';
+import { useEffect } from 'react';
+import { appModulePublicSignIn } from './appModulePublicSignIn';
+import { mockApis } from '@backstage/test-utils';
+import { createApp } from '@backstage/frontend-defaults';
+
+describe('appModulePublicSignIn', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should render a sign-in page', async () => {
+ const app = createApp({
+ configLoader: async () => ({ config: mockApis.config() }),
+ features: [
+ appModulePublicSignIn,
+ createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ SignInPageBlueprint.make({
+ params: {
+ loader: async () => () => Sign in page
,
+ },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ render(app.createRoot());
+
+ await expect(
+ screen.findByText('Sign in page'),
+ ).resolves.toBeInTheDocument();
+ });
+
+ it('should render the form redirect on sign-in', async () => {
+ const submitSpy = jest
+ .spyOn(HTMLFormElement.prototype, 'submit')
+ .mockReturnValue();
+
+ const app = createApp({
+ configLoader: async () => ({ config: mockApis.config() }),
+ features: [
+ appModulePublicSignIn,
+ createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ SignInPageBlueprint.make({
+ params: {
+ loader:
+ async () =>
+ ({ onSignInSuccess }) => {
+ useEffect(() => {
+ onSignInSuccess(
+ mockApis.identity({ token: 'mock-token' }),
+ );
+ }, [onSignInSuccess]);
+ return ;
+ },
+ },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const { baseElement } = render(app.createRoot());
+
+ await waitFor(() => {
+ expect(submitSpy).toHaveBeenCalled();
+ });
+
+ expect(baseElement).toMatchInlineSnapshot(`
+
+
+
+
+
+ `);
+ });
+});
diff --git a/plugins/app/src/alpha/appModulePublicSignIn.tsx b/plugins/app/src/alpha/appModulePublicSignIn.tsx
new file mode 100644
index 0000000000..4a188d1d5f
--- /dev/null
+++ b/plugins/app/src/alpha/appModulePublicSignIn.tsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import appPlugin from '@backstage/plugin-app';
+import { useAsync, useMountEffect } from '@react-hookz/web';
+import {
+ coreExtensionData,
+ createFrontendModule,
+ identityApiRef,
+ useApi,
+} from '@backstage/frontend-plugin-api';
+
+// This is a copy of the CookieAuthRedirect component from the auth-react
+// plugin, to avoid a dependency on that package. Long-term we want this to be
+// the only implementation and remove the one in auth-react once the old frontend system is gone.
+
+// TODO(Rugvip): Should this be part of the app plugin instead? since it owns the backend part of it.
+
+/** @internal */
+export function InternalCookieAuthRedirect() {
+ const identityApi = useApi(identityApiRef);
+
+ const [state, actions] = useAsync(async () => {
+ const { token } = await identityApi.getCredentials();
+ if (!token) {
+ throw new Error('Expected Backstage token in sign-in response');
+ }
+ return token;
+ });
+
+ useMountEffect(actions.execute);
+
+ if (state.status === 'error' && state.error) {
+ return <>An error occurred: {state.error.message}>;
+ }
+
+ if (state.status === 'success' && state.result) {
+ return (
+
+ );
+ }
+
+ return null;
+}
+
+/**
+ * This module is intended for use in public sign-in page apps, in the
+ * `index-public-experimental.tsx` file.
+ *
+ * @remarks
+ *
+ * This module is used to enable the public sign-in flow where the build output
+ * is split into one small publicly accessible app, and the full app protected
+ * by auth.
+ *
+ * This module overrides the `app/layout` extension, which means that most
+ * extension typically installed in an app will be ignored. However, you can
+ * still for example install API and root element extensions.
+ *
+ * A typical setup of this app will only install a custom sign-in page.
+ *
+ * @example
+ *
+ *#### In `index-public-experimental.tsx`
+ *
+ *```ts
+ *import { createApp } from '@backstage/frontend-defaults
+ *import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
+ *import { appModuleSignInPage } from './appModuleSignInPage';
+ *
+ *const app = createApp({
+ * features: [appModuleSignInPage, appModulePublicSignIn],
+ *});
+ *```
+ *
+ *#### In `appModuleSignInPage.tsx`
+ *
+ *```tsx
+ *import { createFrontendModule, SignInPageBlueprint } from '@backstage/frontend-plugin-api';
+ *
+ *export const appModuleSignInPage = createFrontendModule({
+ * pluginId: 'app',
+ * extensions: [
+ * SignInPageBlueprint.make({
+ * params: {
+ * ...
+ * }
+ * }),
+ * ],
+ *})
+ *```
+ *
+ * @alpha
+ */
+export const appModulePublicSignIn = createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ appPlugin.getExtension('app/layout').override({
+ factory: () => [
+ coreExtensionData.reactElement(),
+ ],
+ }),
+ ],
+});
diff --git a/plugins/app/src/alpha/index.ts b/plugins/app/src/alpha/index.ts
new file mode 100644
index 0000000000..8ffa92e8aa
--- /dev/null
+++ b/plugins/app/src/alpha/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { appModulePublicSignIn } from './appModulePublicSignIn';
diff --git a/yarn.lock b/yarn.lock
index 2a5d91cef6..7871bdc8c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4743,15 +4743,18 @@ __metadata:
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
+ "@backstage/frontend-defaults": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
+ "@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.61"
+ "@react-hookz/web": "npm:^24.0.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.0.0"