diff --git a/.changeset/prepare-specialized-app-signin-flow.md b/.changeset/prepare-specialized-app-signin-flow.md
index 03f37b7107..31d5bb1b8d 100644
--- a/.changeset/prepare-specialized-app-signin-flow.md
+++ b/.changeset/prepare-specialized-app-signin-flow.md
@@ -3,4 +3,4 @@
'@backstage/frontend-defaults': patch
---
-Adds `prepareSpecializedApp` as a new two-phase app wiring API for rendering a sign-in page before full app finalization. The existing `createSpecializedApp` API is now deprecated and backed by `prepareSpecializedApp().finalize()`, while `createApp` has been updated to use the same prepare/finalize flow.
+Adds `prepareSpecializedApp` as a new two-phase app wiring API for rendering a sign-in page before full app finalization. Session preparation now resolves to an opaque reusable `sessionState`, which is returned from `getSignIn().ready` and from `finalize()`, and can be passed into a future `prepareSpecializedApp` call to skip sign-in and reuse the prepared session. The existing `createSpecializedApp` API is now deprecated and backed by `prepareSpecializedApp().finalize()`, while `createApp` has been updated to use the same prepare/finalize flow.
diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx
index 788fc6bbc2..cd1356d4cb 100644
--- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx
+++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx
@@ -1731,107 +1731,6 @@ describe('createSpecializedApp', () => {
);
});
- it('should reuse predicate context gathered during sign-in completion', async () => {
- const identityApi = {
- getProfileInfo: async () => ({ displayName: 'Test User' }),
- getBackstageIdentity: async () => ({
- type: 'user' as const,
- userEntityRef: 'user:default/test-user',
- ownershipEntityRefs: ['user:default/test-user'],
- }),
- getCredentials: async () => ({ token: 'token' }),
- signOut: async () => {},
- };
- const featureFlagsApi = {
- isActive: jest.fn((name: string) => name === 'test-flag'),
- registerFlag: jest.fn(),
- getRegisteredFlags: () => [],
- save: jest.fn(),
- } as unknown as typeof featureFlagsApiRef.T;
- const gatedAppPlugin = appPluginOriginal.withOverrides({
- extensions: [
- appPluginOriginal.getExtension('app/layout').override({
- if: { featureFlags: { $contains: 'test-flag' } },
- factory: () => [
- coreExtensionData.reactElement(
Flagged Layout
),
- ],
- }),
- ],
- });
-
- const preparedApp = prepareSpecializedApp({
- features: [
- gatedAppPlugin,
- createFrontendModule({
- pluginId: 'app',
- extensions: [
- ApiBlueprint.make({
- params: defineParams =>
- defineParams({
- api: featureFlagsApiRef,
- deps: {},
- factory: () => featureFlagsApi,
- }),
- }),
- gatedAppPlugin.getExtension('sign-in-page:app').override({
- factory: () => {
- function SignInPage(props: {
- onSignInSuccess(identity: IdentityApi): void;
- }) {
- useEffect(() => {
- props.onSignInSuccess(identityApi);
- }, [props]);
- return Custom Sign In
;
- }
-
- return [signInPageComponentDataRef(SignInPage)];
- },
- }),
- ],
- }),
- ],
- });
-
- const signIn = preparedApp.getSignIn();
- render(signIn!.element);
- await expect(
- screen.findByText('Custom Sign In'),
- ).resolves.toBeInTheDocument();
-
- await signIn!.complete;
- expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag');
- expect(featureFlagsApi.isActive).toHaveBeenCalledTimes(1);
-
- const finalizedApp = preparedApp.finalize();
- render(
- finalizedApp.tree.root.instance!.getData(
- coreExtensionData.reactElement,
- ),
- );
-
- expect(screen.getByText('Flagged Layout')).toBeInTheDocument();
- });
-
- it('should reject bootstrap-visible extensions that use if predicates', () => {
- expect(() =>
- prepareSpecializedApp({
- features: [
- appPluginOriginal,
- createFrontendModule({
- pluginId: 'app',
- extensions: [
- appPluginOriginal.getExtension('sign-in-page:app').override({
- if: { featureFlags: { $contains: 'test-flag' } },
- }),
- ],
- }),
- ],
- }),
- ).toThrow(
- "Extension 'sign-in-page:app' uses 'if' before the session boundary at 'app/root.children'. Move it behind the session boundary or remove the predicate.",
- );
- });
-
it('should gate finalize behind internal async sign-in finalization', async () => {
const identityApi = {
getProfileInfo: async () => ({ displayName: 'Test User' }),
diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx
index 50f8db64f7..75d33282e7 100644
--- a/packages/frontend-defaults/src/createApp.tsx
+++ b/packages/frontend-defaults/src/createApp.tsx
@@ -126,17 +126,18 @@ export function createApp(options?: CreateAppOptions): {
bindRoutes: options?.bindRoutes,
advanced: options?.advanced,
});
+ const signIn = preparedApp.getSignIn();
- if (preparedApp.getSignIn()) {
+ if (signIn.element) {
return {
default: () => ,
};
}
- await preparedApp.buildPredicateContext();
+ const { sessionState } = await signIn.ready;
return {
- default: () => renderFinalizedApp(preparedApp.finalize()),
+ default: () => renderFinalizedApp(preparedApp.finalize(sessionState)),
};
}
@@ -166,15 +167,11 @@ function PreparedAppRoot(props: {
let cancelled = false;
const runFinalize = async () => {
try {
- if (signIn) {
- await signIn.complete;
- } else {
- await props.preparedApp.buildPredicateContext();
- }
+ const { sessionState } = await signIn.ready;
if (cancelled) {
return;
}
- setFinalizedApp(props.preparedApp.finalize());
+ setFinalizedApp(props.preparedApp.finalize(sessionState));
} catch (error) {
if (cancelled) {
return;
@@ -193,7 +190,7 @@ function PreparedAppRoot(props: {
}
if (!finalizedApp) {
- return signIn?.element ?? <>>;
+ return signIn.element ?? <>>;
}
return renderFinalizedApp(finalizedApp);