From f4c03772a46042ff32da0f8241e1b2c889104bd8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Mar 2026 21:59:25 +0100 Subject: [PATCH] frontend-app-api: internalize prepared session state Move prepared app session ownership into frontend-app-api so bootstrap only signals readiness while callers use tryFinalize or finalize to read the finalized app state. This reduces createApp boilerplate and keeps the specialized app lifecycle centered in the lower-level API. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../prepare-specialized-app-signin-flow.md | 2 +- packages/frontend-app-api/src/wiring/index.ts | 3 + packages/frontend-defaults/src/createApp.tsx | 65 ++++++------------- 3 files changed, 24 insertions(+), 46 deletions(-) diff --git a/.changeset/prepare-specialized-app-signin-flow.md b/.changeset/prepare-specialized-app-signin-flow.md index 31d5bb1b8d..746df0d4fe 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. 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. +Adds `prepareSpecializedApp` as a new two-phase app wiring API for rendering a sign-in page before full app finalization. The sign-in step is now exposed as a bootstrap component whose `onReady` callback signals that the prepared app can now be finalized, while the opaque reusable `sessionState` is stored internally on the prepared app and returned from `tryFinalize()` and `finalize()`. That session state can also 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/index.ts b/packages/frontend-app-api/src/wiring/index.ts index 56851ac88a..d4db40bfee 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -15,8 +15,11 @@ */ export { + type FinalizedSpecializedApp, prepareSpecializedApp, type PreparedSpecializedApp, + type PreparedSpecializedAppSignInProps, + type SpecializedAppSessionState, createSpecializedApp, type CreateSpecializedAppOptions, } from './createSpecializedApp'; diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 75d33282e7..38eadf6509 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JSX, lazy, ReactNode, Suspense, useEffect, useState } from 'react'; +import { JSX, lazy, ReactNode, Suspense, useReducer, useState } from 'react'; import { ConfigApi, coreExtensionData, @@ -30,6 +30,7 @@ import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseU import { ConfigReader } from '@backstage/config'; import { CreateAppRouteBinder, + FinalizedSpecializedApp, prepareSpecializedApp, PreparedSpecializedApp, FrontendPluginInfoResolver, @@ -126,18 +127,9 @@ export function createApp(options?: CreateAppOptions): { bindRoutes: options?.bindRoutes, advanced: options?.advanced, }); - const signIn = preparedApp.getSignIn(); - - if (signIn.element) { - return { - default: () => , - }; - } - - const { sessionState } = await signIn.ready; return { - default: () => renderFinalizedApp(preparedApp.finalize(sessionState)), + default: () => , }; } @@ -158,51 +150,34 @@ function PreparedAppRoot(props: { preparedApp: PreparedSpecializedApp; }): JSX.Element { const signIn = props.preparedApp.getSignIn(); + const SignIn = signIn.Component; const [finalizeError, setFinalizeError] = useState(); - const [finalizedApp, setFinalizedApp] = useState< - ReturnType | undefined - >(undefined); - - useEffect(() => { - let cancelled = false; - const runFinalize = async () => { - try { - const { sessionState } = await signIn.ready; - if (cancelled) { - return; - } - setFinalizedApp(props.preparedApp.finalize(sessionState)); - } catch (error) { - if (cancelled) { - return; - } - setFinalizeError(error as Error); - } - }; - void runFinalize(); - return () => { - cancelled = true; - }; - }, [props.preparedApp, signIn]); + const [, triggerRerender] = useReducer((count: number) => count + 1, 0); if (finalizeError) { throw finalizeError; } + const finalizedApp: FinalizedSpecializedApp | undefined = + props.preparedApp.tryFinalize(); + if (!finalizedApp) { - return signIn.element ?? <>; + return ( + { + triggerRerender(); + }} + onError={setFinalizeError} + /> + ); } - return renderFinalizedApp(finalizedApp); -} - -function renderFinalizedApp( - app: ReturnType, -) { - const errorPage = maybeCreateErrorPage(app); + const errorPage = maybeCreateErrorPage(finalizedApp); if (errorPage) { return errorPage; } - return app.tree.root.instance!.getData(coreExtensionData.reactElement)!; + return finalizedApp.tree.root.instance!.getData( + coreExtensionData.reactElement, + )!; }