From 1ccbb86a77c7f3b6395e6fa4ea659bcb44c2a52d Mon Sep 17 00:00:00 2001 From: Shubham Raghav Date: Thu, 20 Aug 2026 13:29:32 +0530 Subject: [PATCH] feat: migrate services to Angular 22 @Service() Replaces `@Injectable({ providedIn: 'root' })` with `@Service()` across the 13 services that used it. `@Service()` is automatically available in the DI system, so the two are equivalent. Two of them used constructor injection, which `@Service()` rejects with NG2028, so those now inject their dependencies as fields -- the pattern the rest of the codebase already uses. LayoutService keeps its constructor body; only the parameters moved, and field initialisers still run before it. Services declared with a bare `@Injectable()` are left alone: they are not auto-provided, so they would need `@Service({ autoProvided: false })`, which is a different change from the one this issue describes. Resolves #7669 Co-Authored-By: Claude Opus 5 --- apps/client/src/app/core/auth.guard.ts | 4 ++-- apps/client/src/app/core/layout.service.ts | 13 +++++-------- apps/client/src/app/services/cache.service.ts | 8 +++----- apps/client/src/app/services/ics/ics.service.ts | 6 ++---- .../app/services/impersonation-storage.service.ts | 6 ++---- .../src/app/services/import-activities.service.ts | 6 ++---- .../src/app/services/settings-storage.service.ts | 6 ++---- .../src/app/services/token-storage.service.ts | 6 ++---- apps/client/src/app/services/user/user.service.ts | 6 ++---- apps/client/src/app/services/web-authn.service.ts | 6 ++---- .../entity-logo/entity-logo-image-source.service.ts | 8 +++----- libs/ui/src/lib/services/admin.service.ts | 6 ++---- libs/ui/src/lib/services/data.service.ts | 6 ++---- 13 files changed, 31 insertions(+), 56 deletions(-) diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index 6ac3417db..91a14d135 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -3,7 +3,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes'; import { DataService } from '@ghostfolio/ui/services'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { ActivatedRouteSnapshot, Router, @@ -12,7 +12,7 @@ import { import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; -@Injectable({ providedIn: 'root' }) +@Service() export class AuthGuard { private readonly dataService = inject(DataService); private readonly router = inject(Router); diff --git a/apps/client/src/app/core/layout.service.ts b/apps/client/src/app/core/layout.service.ts index 2624c4dd6..a2bb7f252 100644 --- a/apps/client/src/app/core/layout.service.ts +++ b/apps/client/src/app/core/layout.service.ts @@ -1,24 +1,21 @@ import { NotificationService } from '@ghostfolio/ui/notifications'; -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Observable, Subject } from 'rxjs'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class LayoutService { public static readonly DEFAULT_NOTIFICATION_MAX_WIDTH = '50rem'; public static readonly DEFAULT_NOTIFICATION_WIDTH = '75vw'; public shouldReloadContent$: Observable; + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly notificationService = inject(NotificationService); private shouldReloadSubject = new Subject(); - public constructor( - private deviceDetectorService: DeviceDetectorService, - private notificationService: NotificationService - ) { + public constructor() { this.shouldReloadContent$ = this.shouldReloadSubject.asObservable(); const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; diff --git a/apps/client/src/app/services/cache.service.ts b/apps/client/src/app/services/cache.service.ts index 69a85f926..5472f9a5c 100644 --- a/apps/client/src/app/services/cache.service.ts +++ b/apps/client/src/app/services/cache.service.ts @@ -1,11 +1,9 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class CacheService { - public constructor(private http: HttpClient) {} + private readonly http = inject(HttpClient); public flush() { return this.http.post(`/api/v1/cache/flush`, {}); diff --git a/apps/client/src/app/services/ics/ics.service.ts b/apps/client/src/app/services/ics/ics.service.ts index a3235380e..d97b427c9 100644 --- a/apps/client/src/app/services/ics/ics.service.ts +++ b/apps/client/src/app/services/ics/ics.service.ts @@ -1,13 +1,11 @@ import { capitalize } from '@ghostfolio/common/helper'; import { ExportResponse } from '@ghostfolio/common/interfaces'; -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { Type } from '@prisma/client'; import { format, parseISO } from 'date-fns'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class IcsService { private readonly ICS_DATE_FORMAT = 'yyyyMMdd'; private readonly ICS_LINE_BREAK = '\r\n'; diff --git a/apps/client/src/app/services/impersonation-storage.service.ts b/apps/client/src/app/services/impersonation-storage.service.ts index fb101e220..1cbba6444 100644 --- a/apps/client/src/app/services/impersonation-storage.service.ts +++ b/apps/client/src/app/services/impersonation-storage.service.ts @@ -1,11 +1,9 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; export const IMPERSONATION_KEY = 'impersonationId'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class ImpersonationStorageService { private hasImpersonationChangeSubject = new BehaviorSubject( this.getId() diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index b73676252..5c06b0e13 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -13,16 +13,14 @@ import { import { Activity } from '@ghostfolio/common/interfaces'; import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { Account, DataSource, Type as ActivityType } from '@prisma/client'; import { isFinite, isNumber, isString } from 'lodash'; import { parse as csvToJson } from 'papaparse'; import { firstValueFrom } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class ImportActivitiesService { private static ACCOUNT_KEYS = ['account', 'accountid']; private static COMMENT_KEYS = ['comment', 'note']; diff --git a/apps/client/src/app/services/settings-storage.service.ts b/apps/client/src/app/services/settings-storage.service.ts index 681552cf7..095c11587 100644 --- a/apps/client/src/app/services/settings-storage.service.ts +++ b/apps/client/src/app/services/settings-storage.service.ts @@ -1,12 +1,10 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; export const KEY_RANGE = 'range'; export const KEY_STAY_SIGNED_IN = 'staySignedIn'; export const KEY_TOKEN = 'auth-token'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class SettingsStorageService { public getSetting(aKey: string): string | null { return window.localStorage.getItem(aKey); diff --git a/apps/client/src/app/services/token-storage.service.ts b/apps/client/src/app/services/token-storage.service.ts index b29fd207b..fe36043d3 100644 --- a/apps/client/src/app/services/token-storage.service.ts +++ b/apps/client/src/app/services/token-storage.service.ts @@ -1,10 +1,8 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { KEY_TOKEN } from './settings-storage.service'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class TokenStorageService { public getToken(): string | null { return ( diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index d58f77c3e..38e9bebc9 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -3,7 +3,7 @@ import { Filter, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HttpClient } from '@angular/common/http'; -import { computed, DestroyRef, inject, Injectable } from '@angular/core'; +import { computed, DestroyRef, inject, Service } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatDialog } from '@angular/material/dialog'; import { ObservableStore } from '@codewithdan/observable-store'; @@ -18,9 +18,7 @@ import { GfSubscriptionInterstitialDialogComponent } from '../../components/subs import { UserStoreActions } from './user-store.actions'; import { UserStoreState } from './user-store.state'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class UserService extends ObservableStore { private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType diff --git a/apps/client/src/app/services/web-authn.service.ts b/apps/client/src/app/services/web-authn.service.ts index 24eee5dc1..b769e8506 100644 --- a/apps/client/src/app/services/web-authn.service.ts +++ b/apps/client/src/app/services/web-authn.service.ts @@ -6,7 +6,7 @@ import { } from '@ghostfolio/common/interfaces'; import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { startAuthentication, startRegistration @@ -14,9 +14,7 @@ import { import { of } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class WebAuthnService { private static readonly WEB_AUTH_N_DEVICE_ID = 'WEB_AUTH_N_DEVICE_ID'; diff --git a/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts b/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts index db916a34f..1b4e3dfa8 100644 --- a/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts +++ b/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts @@ -1,11 +1,9 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; -@Injectable({ - // Required to allow mocking in Storybook - providedIn: 'root' -}) +// Required to allow mocking in Storybook +@Service() export class EntityLogoImageSourceService { public getLogoUrlByAssetProfileIdentifier({ dataSource, diff --git a/libs/ui/src/lib/services/admin.service.ts b/libs/ui/src/lib/services/admin.service.ts index a1735f9de..aa048faaf 100644 --- a/libs/ui/src/lib/services/admin.service.ts +++ b/libs/ui/src/lib/services/admin.service.ts @@ -24,14 +24,12 @@ import { DateRange } from '@ghostfolio/common/types'; import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { AssetProfileSplit, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; import { isNumber } from 'lodash'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class AdminService { private readonly environment = inject(GF_ENVIRONMENT); private readonly http = inject(HttpClient); diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 7081f9b2c..fb9376041 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -66,7 +66,7 @@ import type { import { translate } from '@ghostfolio/ui/i18n'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { utc } from '@date-fns/utc'; import { @@ -85,9 +85,7 @@ import { cloneDeep, groupBy, isNumber } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class DataService { private readonly http = inject(HttpClient);