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);