diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx
index cd1356d4cb..1695897a14 100644
--- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx
+++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx
@@ -1731,6 +1731,86 @@ 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');
+
+ const finalizedApp = preparedApp.finalize();
+ render(
+ finalizedApp.tree.root.instance!.getData(
+ coreExtensionData.reactElement,
+ ),
+ );
+
+ expect(screen.getByText('Flagged Layout')).toBeInTheDocument();
+ });
+
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.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx
index af8517624a..540fb5e41c 100644
--- a/packages/frontend-defaults/src/createApp.test.tsx
+++ b/packages/frontend-defaults/src/createApp.test.tsx
@@ -386,6 +386,56 @@ describe('createApp', () => {
).resolves.toBeInTheDocument();
});
+ it('should evaluate extension if predicates before rendering apps without sign-in', 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 app = createApp({
+ advanced: {
+ configLoader: async () => ({ config: mockApis.config() }),
+ },
+ features: [
+ appPlugin,
+ createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ ApiBlueprint.make({
+ params: defineParams =>
+ defineParams({
+ api: featureFlagsApiRef,
+ deps: {},
+ factory: () => featureFlagsApi,
+ }),
+ }),
+ ],
+ }),
+ createFrontendPlugin({
+ pluginId: 'test',
+ featureFlags: [{ name: 'test-flag' }],
+ extensions: [
+ PageBlueprint.make({
+ if: { featureFlags: { $contains: 'test-flag' } },
+ params: {
+ path: '/',
+ loader: async () => Flagged Page
,
+ },
+ }),
+ ],
+ }),
+ ],
+ });
+
+ await renderWithEffects(app.createRoot());
+
+ await expect(
+ screen.findByText('Flagged Page'),
+ ).resolves.toBeInTheDocument();
+ expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag');
+ });
+
it('should make the app structure available through the AppTreeApi', async () => {
let appTreeApi: AppTreeApi | undefined = undefined;
diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx
index 8a47088f62..50f8db64f7 100644
--- a/packages/frontend-defaults/src/createApp.tsx
+++ b/packages/frontend-defaults/src/createApp.tsx
@@ -133,6 +133,8 @@ export function createApp(options?: CreateAppOptions): {
};
}
+ await preparedApp.buildPredicateContext();
+
return {
default: () => renderFinalizedApp(preparedApp.finalize()),
};
@@ -164,12 +166,15 @@ function PreparedAppRoot(props: {
let cancelled = false;
const runFinalize = async () => {
try {
- const predicateContext =
+ if (signIn) {
+ await signIn.complete;
+ } else {
await props.preparedApp.buildPredicateContext();
+ }
if (cancelled) {
return;
}
- setFinalizedApp(props.preparedApp.finalize(predicateContext));
+ setFinalizedApp(props.preparedApp.finalize());
} catch (error) {
if (cancelled) {
return;
@@ -177,23 +182,7 @@ function PreparedAppRoot(props: {
setFinalizeError(error as Error);
}
};
- if (signIn) {
- void signIn.complete
- .then(() => {
- if (cancelled) {
- return;
- }
- void runFinalize();
- })
- .catch(error => {
- if (cancelled) {
- return;
- }
- setFinalizeError(error);
- });
- } else {
- setFinalizedApp(props.preparedApp.finalize());
- }
+ void runFinalize();
return () => {
cancelled = true;
};