Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.11.0] - 2026-05-20

### Changed
- **BREAKING:** All 16 services now require explicit registration via `provideX()` factory functions
- Removed `providedIn: 'root'` from 12 services: ErrorService, FeatureFlagService, NavigationService, BreadcrumbService, PermissionService, AlertService, MasterDataService, TransportRegistry, ApiClient, AuthService, UserContextService, SessionService
- Updated 7 existing factories to register their service class (provideErrorHandling, provideFeatureFlags, provideNavigation, providePermissions, provideAlerts, provideMasterData, provideFireflyTransport)

### Added
- `provideAuth()` — factory for AuthService registration
- `provideSession(config?)` — factory for SessionService registration (accepts optional `SessionTimeoutConfig`)
- `provideBreadcrumb()` — factory for BreadcrumbService registration
- `provideApiClient()` — factory for ApiClient registration
- `provideUserContext()` — factory for UserContextService registration

### Migration
- Applications MUST add `provideAuth()`, `provideSession()`, `provideBreadcrumb()`, `provideApiClient()`, and `provideUserContext()` to their `app.config.ts` providers
- Tests that inject services directly MUST include the corresponding `provideX()` in `TestBed.configureTestingModule({ providers: [...] })`

## [0.10.1] - 2026-05-20

### Added
Expand Down Expand Up @@ -154,7 +172,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- User context module: `UserContextService`
- API runtime module: `ApiClient`, `TransportRegistry`, `HttpTransportAdapter`

[Unreleased]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.10.0...HEAD
[Unreleased]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.11.0...HEAD
[0.11.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.10.1...core@0.11.0
[0.10.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.9.0...core@0.10.0
[0.9.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.8.0...core@0.9.0
[0.8.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.7.2...core@0.8.0
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fireflyframework/core",
"version": "0.10.1",
"version": "0.11.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/lib/alerts/alert.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { AlertService } from './alert.service';
import { provideAlerts } from './provide-alerts';

describe('AlertService — toasts & banners', () => {
let service: AlertService;

beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({});
TestBed.configureTestingModule({ providers: [provideAlerts()] });
service = TestBed.inject(AlertService);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/alerts/alert.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const MAX_VISIBLE_BANNERS = 3;
* // @for (toast of alerts.activeToasts(); track toast.id) { ... }
* ```
*/
@Injectable({ providedIn: 'root' })
@Injectable()
export class AlertService {
private readonly _toasts = signal<Toast[]>([]);
private readonly _banners = signal<Banner[]>([]);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/lib/alerts/provide-alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
makeEnvironmentProviders,
} from '@angular/core';
import { AlertConfig } from './alert.types';
import { AlertService } from './alert.service';

/**
* Injection token for optional alert configuration.
Expand Down Expand Up @@ -36,6 +37,7 @@ export function provideAlerts(
config?: AlertConfig,
): EnvironmentProviders {
return makeEnvironmentProviders([
AlertService,
...(config ? [{ provide: ALERT_CONFIG, useValue: config }] : []),
]);
}
4 changes: 3 additions & 1 deletion packages/core/src/lib/api-runtime/api-client.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { TransportRegistry } from './transport/transport-registry';
import { TransportAdapter } from './transport/transport-adapter';
import { TransportError } from './transport/transport-error';
import { TransportProtocol, TransportRequest, TransportResponse } from './transport/transport-request';
import { provideApiClient } from './provide-api-client';
import { provideFireflyTransport } from './provide-firefly-transport';

/** Minimal mock adapter for testing */
class MockTransportAdapter extends TransportAdapter {
Expand All @@ -29,7 +31,7 @@ describe('ApiClient', () => {
let mockAdapter: MockTransportAdapter;

beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({ providers: [provideApiClient(), provideFireflyTransport({ defaultProtocol: 'http', routes: [] })] });
apiClient = TestBed.inject(ApiClient);
registry = TestBed.inject(TransportRegistry);

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/api-runtime/api-client.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { TransportError } from './transport/transport-error';
* - Retry (that's RetryInterceptor cross-protocol, G14)
* - Handle auth headers (that's SecurityInterceptor)
*/
@Injectable({ providedIn: 'root' })
@Injectable()
export class ApiClient {
private readonly registry = inject(TransportRegistry);

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/lib/api-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './transport';
export * from './adapters/http';
export { ApiClient } from './api-client.service';
export { provideFireflyTransport } from './provide-firefly-transport';
export { provideApiClient } from './provide-api-client';
6 changes: 6 additions & 0 deletions packages/core/src/lib/api-runtime/provide-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core';
import { ApiClient } from './api-client.service';

export function provideApiClient(): EnvironmentProviders {
return makeEnvironmentProviders([ApiClient]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { TRANSPORT_OPTIONS } from './transport/retry.interceptor';
*/
export function provideFireflyTransport(config: TransportConfig): EnvironmentProviders {
return makeEnvironmentProviders([
TransportRegistry,
...(config.options ? [{ provide: TRANSPORT_OPTIONS, useValue: config.options }] : []),
provideEnvironmentInitializer(() => {
const registry = inject(TransportRegistry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TransportAdapter } from './transport-adapter';
import { TransportError } from './transport-error';
import { TransportProtocol, TransportRequest, TransportResponse } from './transport-request';
import { TransportRoute } from './transport-route';
import { provideFireflyTransport } from '../provide-firefly-transport';

/** Minimal mock adapter for testing */
class MockTransportAdapter extends TransportAdapter {
Expand All @@ -30,7 +31,7 @@ describe('TransportRegistry', () => {
let registry: TransportRegistry;

beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({ providers: [provideFireflyTransport({ defaultProtocol: 'http', routes: [] })] });
registry = TestBed.inject(TransportRegistry);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { TransportRoute, ResolvedTransport } from './transport-route';
* configuration and adapters are registered in the providers.
* No dynamic re-routing at runtime.
*/
@Injectable({ providedIn: 'root' })
@Injectable()
export class TransportRegistry {
/** Configured routes — loaded from firefly.config.yaml by provideFireflyTransport() */
private readonly routes = signal<TransportRoute[]>([]);
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/lib/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { AuthService } from './auth.service';
import { AuthTokens } from './auth.types';
import { SessionService } from '../session/session.service';
import { UserContextService } from '../user-context/user-context.service';
import { provideAuth } from './provide-auth';
import { provideSession } from '../session/provide-session';
import { provideUserContext } from '../user-context/provide-user-context';

const API = '/api/v1/experience/security';

Expand Down Expand Up @@ -47,6 +50,9 @@ describe('AuthService', () => {

TestBed.configureTestingModule({
providers: [
provideAuth(),
provideSession(),
provideUserContext(),
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const AUTH_API_BASE = '/api/v1/experience/security';
*
* API base: /api/v1/experience/security (exp-security microservice)
*/
@Injectable({ providedIn: 'root' })
@Injectable()
export class AuthService {
/** Reactive signal indicating whether the user is currently authenticated. */
readonly isAuthenticated = signal(false);
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/lib/auth/guards/auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { provideHttpClientTesting } from '@angular/common/http/testing';
import { Component } from '@angular/core';
import { authGuard } from './auth.guard';
import { AuthService } from '../auth.service';
import { provideAuth } from '../provide-auth';
import { provideSession } from '../../session/provide-session';
import { provideUserContext } from '../../user-context/provide-user-context';

@Component({ template: '', standalone: true })
class DummyComponent {}
Expand All @@ -18,6 +21,9 @@ describe('authGuard', () => {

TestBed.configureTestingModule({
providers: [
provideAuth(),
provideSession(),
provideUserContext(),
provideHttpClient(),
provideHttpClientTesting(),
provideRouter([
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { AuthService } from './auth.service';
export { authInterceptor } from './interceptors/auth.interceptor';
export { authGuard } from './guards/auth.guard';
export type { LoginCredentials, AuthTokens, AuthResult } from './auth.types';
export { provideAuth } from './provide-auth';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
provideHttpClientTesting,
} from '@angular/common/http/testing';
import { authInterceptor } from './auth.interceptor';
import { provideAuth } from '../provide-auth';
import { provideSession } from '../../session/provide-session';
import { provideUserContext } from '../../user-context/provide-user-context';

/** Flush Promise microtask queue so async interceptor logic completes. */
const flushMicrotasks = () => new Promise((r) => setTimeout(r, 0));
Expand All @@ -22,6 +25,9 @@ describe('authInterceptor', () => {

TestBed.configureTestingModule({
providers: [
provideAuth(),
provideSession(),
provideUserContext(),
provideHttpClient(withInterceptors([authInterceptor])),
provideHttpClientTesting(),
],
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/lib/auth/provide-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core';
import { AuthService } from './auth.service';

export function provideAuth(): EnvironmentProviders {
return makeEnvironmentProviders([AuthService]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@angular/common/http/testing';
import { errorInterceptor } from './error.interceptor';
import { ErrorService } from './error.service';
import { provideErrorHandling } from './provide-error-handling';

describe('errorInterceptor', () => {
let http: HttpClient;
Expand All @@ -19,6 +20,7 @@ describe('errorInterceptor', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideErrorHandling(),
provideHttpClient(withInterceptors([errorInterceptor])),
provideHttpClientTesting(),
],
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/lib/error-handling/error.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { TestBed } from '@angular/core/testing';
import { ErrorService, ERROR_HANDLING_CONFIG } from './error.service';
import { createAppError } from './app-error';
import type { AppError } from './app-error';
import { provideErrorHandling } from './provide-error-handling';

describe('ErrorService', () => {
let service: ErrorService;

beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({ providers: [provideErrorHandling()] });
service = TestBed.inject(ErrorService);
});

Expand Down Expand Up @@ -106,7 +107,7 @@ describe('ErrorService with custom config', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: ERROR_HANDLING_CONFIG, useValue: { maxHistorySize: 3 } },
provideErrorHandling({ maxHistorySize: 3 }),
],
});
service = TestBed.inject(ErrorService);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/error-handling/error.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const ERROR_HANDLING_CONFIG = new InjectionToken<{ maxHistorySize?: numbe
* this.errors.clearError();
* ```
*/
@Injectable({ providedIn: 'root' })
@Injectable()
export class ErrorService {
private readonly config = inject(ERROR_HANDLING_CONFIG, { optional: true });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing';
import { HttpErrorResponse } from '@angular/common/http';
import { FireflyErrorHandler } from './firefly-error-handler';
import { ErrorService } from './error.service';
import { provideErrorHandling } from './provide-error-handling';

describe('FireflyErrorHandler', () => {
let handler: FireflyErrorHandler;
Expand All @@ -10,7 +11,7 @@ describe('FireflyErrorHandler', () => {

beforeEach(() => {
TestBed.configureTestingModule({
providers: [FireflyErrorHandler],
providers: [provideErrorHandling(), FireflyErrorHandler],
});

handler = TestBed.inject(FireflyErrorHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
makeEnvironmentProviders,
} from '@angular/core';
import type { ErrorHandlingConfig } from './app-error';
import { ERROR_HANDLING_CONFIG } from './error.service';
import { ErrorService, ERROR_HANDLING_CONFIG } from './error.service';
import { FireflyErrorHandler } from './firefly-error-handler';

/**
Expand Down Expand Up @@ -36,6 +36,7 @@ export function provideErrorHandling(
config?: ErrorHandlingConfig,
): EnvironmentProviders {
return makeEnvironmentProviders([
ErrorService,
{ provide: ErrorHandler, useClass: FireflyErrorHandler },
...(config ? [{ provide: ERROR_HANDLING_CONFIG, useValue: config }] : []),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FfFeatureFlagDirective } from './feature-flag.directive';
import { FeatureFlagService } from './feature-flag.service';
import { provideFeatureFlags } from './provide-feature-flags';

@Component({
template: `<span *ffFeatureFlag="'new-dashboard'">Visible</span>`,
Expand All @@ -17,6 +18,7 @@ describe('FfFeatureFlagDirective', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestHostComponent],
providers: [provideFeatureFlags()],
});

service = TestBed.inject(FeatureFlagService);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { FeatureFlagService, FEATURE_FLAG_CONFIG } from './feature-flag.service';
import { provideFeatureFlags } from './provide-feature-flags';

describe('FeatureFlagService', () => {
let service: FeatureFlagService;

beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({ providers: [provideFeatureFlags()] });
service = TestBed.inject(FeatureFlagService);
});

Expand Down Expand Up @@ -338,10 +339,7 @@ describe('FeatureFlagService with config', () => {
it('should load defaults from injected config', () => {
TestBed.configureTestingModule({
providers: [
{
provide: FEATURE_FLAG_CONFIG,
useValue: { defaults: { 'pre-loaded': true } },
},
provideFeatureFlags({ defaults: { 'pre-loaded': true } }),
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const FEATURE_FLAG_CONFIG = new InjectionToken<FeatureFlagConfig>(
* await flags.loadFlags('firebase');
* ```
*/
@Injectable({ providedIn: 'root' })
@Injectable()
export class FeatureFlagService {
private readonly _flags = signal<Map<string, boolean>>(new Map());
private readonly _computedCache = new Map<string, Signal<boolean>>();
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/lib/feature-flags/provide-feature-flags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import type { FeatureFlagConfig } from './feature-flag.types';
import { FEATURE_FLAG_CONFIG } from './feature-flag.service';
import { FeatureFlagService, FEATURE_FLAG_CONFIG } from './feature-flag.service';

/**
* Configure the feature-flags module.
Expand All @@ -27,6 +27,7 @@ export function provideFeatureFlags(
config?: FeatureFlagConfig,
): EnvironmentProviders {
return makeEnvironmentProviders([
FeatureFlagService,
...(config
? [{ provide: FEATURE_FLAG_CONFIG, useValue: config }]
: []),
Expand Down
Loading