diff --git a/.changeset/silver-bobcats-cry.md b/.changeset/silver-bobcats-cry.md new file mode 100644 index 00000000..4578aaf5 --- /dev/null +++ b/.changeset/silver-bobcats-cry.md @@ -0,0 +1,5 @@ +--- +'@openstapps/app': minor +--- + +Queue config update for next launch to not block app launches diff --git a/.changeset/twelve-planes-knock.md b/.changeset/twelve-planes-knock.md new file mode 100644 index 00000000..0eba8dbd --- /dev/null +++ b/.changeset/twelve-planes-knock.md @@ -0,0 +1,5 @@ +--- +'@openstapps/proxy': minor +--- + +Send 426 to outdated clients instead of 404 diff --git a/backend/proxy/config/default.cjs b/backend/proxy/config/default.cjs index 0a57e657..79b38365 100644 --- a/backend/proxy/config/default.cjs +++ b/backend/proxy/config/default.cjs @@ -14,9 +14,8 @@ * along with this program. If not, see . */ -// ESM is not supported, and cts is not detected, so we use type-checked cjs instead. /** @type {import('../src/common').ConfigFile} */ -const configFile = { +module.exports = { activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'], hiddenRoutes: ['/bulk'], logFormat: 'default', @@ -31,5 +30,3 @@ const configFile = { dhparam: '/etc/nginx/certs/dhparam.pem', }, }; - -export default configFile; diff --git a/backend/proxy/package.json b/backend/proxy/package.json index f9e41738..f87b3a07 100644 --- a/backend/proxy/package.json +++ b/backend/proxy/package.json @@ -34,10 +34,12 @@ "build": "tsup-node --dts", "build:docker": "docker build -t openstapps:proxy ../../.deploy/proxy", "deploy": "pnpm --prod --filter=@openstapps/proxy deploy ../../.deploy/proxy", + "dev": "tsup --watch --onSuccess \"pnpm run start\"", "format": "prettier . -c --ignore-path ../../.gitignore", "format:fix": "prettier --write . --ignore-path ../../.gitignore", "lint": "eslint --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/", + "start": "node app.js", "test": "c8 mocha" }, "dependencies": { diff --git a/backend/proxy/src/app.ts b/backend/proxy/src/app.ts index c5a44a73..3da1d7c0 100644 --- a/backend/proxy/src/app.ts +++ b/backend/proxy/src/app.ts @@ -37,7 +37,7 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); * Reads the container information from the docker socket and updates the nginx config if necessary */ async function updateNginxConfig() { - const containers = await getContainers(); + const containers = await getContainers(process.env.DOCKER_SOCKET); const containerHash = containers .map((container: ContainerInfo) => { @@ -78,4 +78,4 @@ async function updateNginxConfig() { // start the process that checks the docker socket periodically // eslint-disable-next-line unicorn/prefer-top-level-await -updateNginxConfig(); +await updateNginxConfig(); diff --git a/examples/minimal-deployment/docker-compose.yml b/examples/minimal-deployment/docker-compose.yml index 960e9437..a189d430 100644 --- a/examples/minimal-deployment/docker-compose.yml +++ b/examples/minimal-deployment/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.7' services: database: - image: registry.gitlab.com/openstapps/openstapps/database:2.0.0 + image: registry.gitlab.com/openstapps/openstapps/database:3.0.0-next.4 volumes: - ./database:/usr/share/elasticsearch/data expose: @@ -9,7 +9,7 @@ services: restart: unless-stopped backend: - image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.0 + image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.4 environment: ES_ADDR: "http://database:9200" NODE_CONFIG_ENV: "elasticsearch" @@ -27,17 +27,17 @@ services: - database api: - image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.0 + image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.4 links: - "backend" minimal-connector: - image: registry.gitlab.com/openstapps/minimal-connector:core-0.23 + image: registry.gitlab.com/openstapps/openstapps/minimal-connector:3.0.0-next.4 container_name: minimal-connector-0.23 command: ["http://backend:3000", "minimal-connector", "f-u"] app: - image: registry.gitlab.com/openstapps/app/executable:core-0.23 + image: registry.gitlab.com/openstapps/openstapps/app:3.0.0-next.4 expose: - 8100 ports: diff --git a/frontend/app/src/app/_helpers/ts-logger.ts b/frontend/app/src/app/_helpers/ts-logger.ts deleted file mode 100644 index 766ab40d..00000000 --- a/frontend/app/src/app/_helpers/ts-logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (C) 2020 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -import {NGXLogger} from 'ngx-logger'; - -export let logger: NGXLogger; - -export const initLogger = (newLogger: NGXLogger) => (logger = newLogger); diff --git a/frontend/app/src/app/app.module.ts b/frontend/app/src/app/app.module.ts index 7f4859b7..3a3c308e 100644 --- a/frontend/app/src/app/app.module.ts +++ b/frontend/app/src/app/app.module.ts @@ -21,17 +21,13 @@ import {RouteReuseStrategy} from '@angular/router'; import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular'; import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core'; import {TranslateHttpLoader} from '@ngx-translate/http-loader'; -import moment from 'moment'; import 'moment/min/locales'; -import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger'; +import {LoggerModule, NgxLoggerLevel} from 'ngx-logger'; import SwiperCore, {FreeMode, Navigation} from 'swiper'; - import {environment} from '../environments/environment'; import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; import {CatalogModule} from './modules/catalog/catalog.module'; -import {ConfigModule} from './modules/config/config.module'; -import {ConfigProvider} from './modules/config/config.provider'; import {DashboardModule} from './modules/dashboard/dashboard.module'; import {DataModule} from './modules/data/data.module'; import {HebisModule} from './modules/hebis/hebis.module'; @@ -40,11 +36,9 @@ import {MenuModule} from './modules/menu/menu.module'; import {NewsModule} from './modules/news/news.module'; import {ScheduleModule} from './modules/schedule/schedule.module'; import {SettingsModule} from './modules/settings/settings.module'; -import {SettingsProvider} from './modules/settings/settings.provider'; import {StorageModule} from './modules/storage/storage.module'; import {ThingTranslateModule} from './translation/thing-translate.module'; import {UtilModule} from './util/util.module'; -import {initLogger} from './_helpers/ts-logger'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {AboutModule} from './modules/about/about.module'; import {FavoritesModule} from './modules/favorites/favorites.module'; @@ -54,80 +48,22 @@ import {DebugDataCollectorService} from './modules/data/debug-data-collector.ser import {AuthModule} from './modules/auth/auth.module'; import {BackgroundModule} from './modules/background/background.module'; import {LibraryModule} from './modules/library/library.module'; -import {StorageProvider} from './modules/storage/storage.provider'; import {AssessmentsModule} from './modules/assessments/assessments.module'; import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor'; -import {RoutingStackService} from './util/routing-stack.service'; -import {SCLanguageCode, SCSettingValue} from '@openstapps/core'; -import {DefaultAuthService} from './modules/auth/default-auth.service'; -import {PAIAAuthService} from './modules/auth/paia/paia-auth.service'; import {IonIconModule} from './util/ion-icon/ion-icon.module'; import {NavigationModule} from './modules/menu/navigation/navigation.module'; import {browserFactory, SimpleBrowser} from './util/browser.factory'; -import {getDateFnsLocale} from './translation/dfns-locale'; -import {setDefaultOptions} from 'date-fns'; -import {DateFnsConfigurationService} from 'ngx-date-fns'; +import {ConfigProvider} from './modules/config/config.provider'; +import {SettingsProvider} from './modules/settings/settings.provider'; +import {TranslateServiceWrapper} from './translation/translate-service-wrapper'; +import {DefaultAuthService} from './modules/auth/default-auth.service'; +import {PAIAAuthService} from './modules/auth/paia/paia-auth.service'; +import {StorageProvider} from './modules/storage/storage.provider'; registerLocaleData(localeDe); SwiperCore.use([FreeMode, Navigation]); -/** - * Initializes data needed on startup - */ -export function initializerFactory( - storageProvider: StorageProvider, - logger: NGXLogger, - settingsProvider: SettingsProvider, - configProvider: ConfigProvider, - translateService: TranslateService, - _routingStackService: RoutingStackService, - defaultAuthService: DefaultAuthService, - paiaAuthService: PAIAAuthService, - dateFnsConfigurationService: DateFnsConfigurationService, -) { - return async () => { - initLogger(logger); - await storageProvider.init(); - await configProvider.init(); - await settingsProvider.init(); - try { - if (configProvider.firstSession) { - // set language from browser - await settingsProvider.setSettingValue( - 'profile', - 'language', - translateService.getBrowserLang() as SCSettingValue, - ); - } - const languageCode = (await settingsProvider.getValue('profile', 'language')) as string; - // this language will be used as a fallback when a translation isn't found in the current language - translateService.setDefaultLang('en'); - translateService.use(languageCode); - moment.locale(languageCode); - const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode); - setDefaultOptions({locale: dateFnsLocale}); - dateFnsConfigurationService.setLocale(dateFnsLocale); - - await defaultAuthService.init(); - await paiaAuthService.init(); - } catch (error) { - logger.warn(error); - } - }; -} - -/** - * TODO - * @param http TODO - */ -export function createTranslateLoader(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/i18n/', '.json'); -} - -/** - * TODO - */ @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], @@ -141,7 +77,6 @@ export function createTranslateLoader(http: HttpClient) { BrowserAnimationsModule, CatalogModule, CommonModule, - ConfigModule, DashboardModule, DataModule, HebisModule, @@ -165,7 +100,9 @@ export function createTranslateLoader(http: HttpClient) { loader: { deps: [HttpClient], provide: TranslateLoader, - useFactory: createTranslateLoader, + useFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); + }, }, }), UtilModule, @@ -175,6 +112,30 @@ export function createTranslateLoader(http: HttpClient) { }), ], providers: [ + { + provide: APP_INITIALIZER, + useFactory: + (...providers: Array<{beforeAppInit(): Promise}>) => + async () => { + for (const provider of providers) { + await provider.beforeAppInit(); + } + }, + // Declare initialization (order matters) + deps: [ + StorageProvider, + ConfigProvider, + SettingsProvider, + TranslateService, + DefaultAuthService, + PAIAAuthService, + ], + multi: true, + }, + { + provide: TranslateService, + useClass: TranslateServiceWrapper, + }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy, @@ -188,22 +149,6 @@ export function createTranslateLoader(http: HttpClient) { useFactory: browserFactory, deps: [Platform], }, - { - provide: APP_INITIALIZER, - multi: true, - deps: [ - StorageProvider, - NGXLogger, - SettingsProvider, - ConfigProvider, - TranslateService, - RoutingStackService, - DefaultAuthService, - PAIAAuthService, - DateFnsConfigurationService, - ], - useFactory: initializerFactory, - }, { provide: HTTP_INTERCEPTORS, useClass: ServiceHandlerInterceptor, diff --git a/frontend/app/src/app/before-app-init.ts b/frontend/app/src/app/before-app-init.ts new file mode 100644 index 00000000..b3968ce1 --- /dev/null +++ b/frontend/app/src/app/before-app-init.ts @@ -0,0 +1,10 @@ +/** + * Services or providers implementing this interface + * must be added to the `APP_INITIALIZER` deps + */ +export interface BeforeAppInit { + /** + * Any logic that has to run before the app is initialized + */ + beforeAppInit(): Promise; +} diff --git a/frontend/app/src/app/modules/about/about-page/about-page.component.ts b/frontend/app/src/app/modules/about/about-page/about-page.component.ts index abe311a0..6257a8dc 100644 --- a/frontend/app/src/app/modules/about/about-page/about-page.component.ts +++ b/frontend/app/src/app/modules/about/about-page/about-page.component.ts @@ -14,10 +14,9 @@ */ import {Component, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; -import {SCAboutPage, SCAppConfiguration} from '@openstapps/core'; -import {ConfigProvider} from '../../config/config.provider'; +import {SCAboutPage} from '@openstapps/core'; import packageJson from '../../../../../package.json'; -import config from 'capacitor.config'; +import {ConfigProvider} from '../../config/config.provider'; @Component({ selector: 'about-page', @@ -27,15 +26,12 @@ import config from 'capacitor.config'; export class AboutPageComponent implements OnInit { content: SCAboutPage; - appName = config.appName; - version = packageJson.version; - constructor(private readonly route: ActivatedRoute, private readonly configProvider: ConfigProvider) {} + constructor(readonly route: ActivatedRoute, readonly config: ConfigProvider) {} - async ngOnInit() { + ngOnInit() { const route = this.route.snapshot.url.map(it => it.path).join('/'); - this.content = - (this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {}; + this.content = this.config.app.aboutPages[route] ?? {}; } } diff --git a/frontend/app/src/app/modules/about/about-page/about-page.html b/frontend/app/src/app/modules/about/about-page/about-page.html index 166c0655..fd220631 100644 --- a/frontend/app/src/app/modules/about/about-page/about-page.html +++ b/frontend/app/src/app/modules/about/about-page/about-page.html @@ -25,7 +25,7 @@ - {{ appName }} v{{ version }} + {{ config.app.name }} v{{ version }}
diff --git a/frontend/app/src/app/modules/about/about.module.ts b/frontend/app/src/app/modules/about/about.module.ts index 5c975fa3..0dc3876d 100644 --- a/frontend/app/src/app/modules/about/about.module.ts +++ b/frontend/app/src/app/modules/about/about.module.ts @@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; -import {ConfigProvider} from '../config/config.provider'; import {AboutPageComponent} from './about-page/about-page.component'; import {MarkdownModule} from 'ngx-markdown'; import {AboutPageContentComponent} from './about-page/about-page-content.component'; @@ -64,6 +63,5 @@ const settingsRoutes: Routes = [ ScrollingModule, UtilModule, ], - providers: [ConfigProvider], }) export class AboutModule {} diff --git a/frontend/app/src/app/modules/assessments/assessments.provider.ts b/frontend/app/src/app/modules/assessments/assessments.provider.ts index b0f94abf..caa559a7 100644 --- a/frontend/app/src/app/modules/assessments/assessments.provider.ts +++ b/frontend/app/src/app/modules/assessments/assessments.provider.ts @@ -13,11 +13,12 @@ * this program. If not, see . */ import {Injectable} from '@angular/core'; -import {ConfigProvider} from '../config/config.provider'; import {SCAssessment, SCUuid} from '@openstapps/core'; import {DefaultAuthService} from '../auth/default-auth.service'; import {HttpClient} from '@angular/common/http'; import {uniqBy, keyBy} from '@openstapps/collection-utils'; +import {firstValueFrom} from 'rxjs'; +import {ConfigProvider} from '../config/config.provider'; /** * @@ -65,7 +66,7 @@ export class AssessmentsProvider { cacheMaxAge = 15 * 60 * 1000; constructor( - readonly configProvider: ConfigProvider, + readonly config: ConfigProvider, readonly defaultAuth: DefaultAuthService, readonly http: HttpClient, ) {} @@ -91,21 +92,20 @@ export class AssessmentsProvider { return this.cache; } - const url = this.configProvider.config.app.features.extern?.hisometry.url; + const url = this.config.app.features.extern?.hisometry.url; if (!url) throw new Error('Config lacks url for hisometry'); - this.cache = this.http - .get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, { + this.cache = firstValueFrom( + this.http.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, { headers: { Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`, }, - }) - .toPromise() - .then(it => { - this.cacheTimestamp = Date.now(); + }), + ).then(it => { + this.cacheTimestamp = Date.now(); - return it?.data ?? []; - }); + return it?.data ?? []; + }); this.assessments = this.cache.then(toAssessmentMap); return this.cache; diff --git a/frontend/app/src/app/modules/auth/auth-helper.service.ts b/frontend/app/src/app/modules/auth/auth-helper.service.ts index 7dfdd726..f806e373 100644 --- a/frontend/app/src/app/modules/auth/auth-helper.service.ts +++ b/frontend/app/src/app/modules/auth/auth-helper.service.ts @@ -12,24 +12,18 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Injectable} from '@angular/core'; import {IPAIAAuthAction} from './paia/paia-auth-action'; import {AuthActions, IAuthAction} from 'ionic-appauth'; import {TranslateService} from '@ngx-translate/core'; import {JSONPath} from 'jsonpath-plus'; -import { - SCAuthorizationProvider, - SCAuthorizationProviderType, - SCUserConfiguration, - SCUserConfigurationMap, -} from '@openstapps/core'; -import {ConfigProvider} from '../config/config.provider'; +import {SCAuthorizationProviderType, SCUserConfiguration, SCUserConfigurationMap} from '@openstapps/core'; import {StorageProvider} from '../storage/storage.provider'; import {DefaultAuthService} from './default-auth.service'; import {PAIAAuthService} from './paia/paia-auth.service'; import {SimpleBrowser} from '../../util/browser.factory'; import {AlertController} from '@ionic/angular'; +import {ConfigProvider} from '../config/config.provider'; const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path'; @@ -37,23 +31,19 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path'; providedIn: 'root', }) export class AuthHelperService { - userConfigurationMap: SCUserConfigurationMap; + get userConfigurationMap(): SCUserConfigurationMap { + return this.config.auth.default!.endpoints.mapping; + } constructor( private translateService: TranslateService, - private configProvider: ConfigProvider, private storageProvider: StorageProvider, private defaultAuth: DefaultAuthService, private paiaAuth: PAIAAuthService, private browser: SimpleBrowser, private alertController: AlertController, - ) { - this.userConfigurationMap = ( - this.configProvider.getAnyValue('auth') as { - default: SCAuthorizationProvider; - } - ).default.endpoints.mapping; - } + private config: ConfigProvider, + ) {} public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) { let message: string | undefined; diff --git a/frontend/app/src/app/modules/auth/auth.service.ts b/frontend/app/src/app/modules/auth/auth.service.ts index cca6a5d1..3f418130 100644 --- a/frontend/app/src/app/modules/auth/auth.service.ts +++ b/frontend/app/src/app/modules/auth/auth.service.ts @@ -175,7 +175,7 @@ export abstract class AuthService implements IAuthService { public async init() { this.setupAuthorizationNotifier(); - this.loadTokenFromStorage(); + await this.loadTokenFromStorage(); this.addActionObserver(this._actionHistory); this.addActionObserver(this._session); } diff --git a/frontend/app/src/app/modules/auth/default-auth.service.ts b/frontend/app/src/app/modules/auth/default-auth.service.ts index 701365a2..e2284551 100644 --- a/frontend/app/src/app/modules/auth/default-auth.service.ts +++ b/frontend/app/src/app/modules/auth/default-auth.service.ts @@ -12,29 +12,26 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import { AuthorizationRequestHandler, AuthorizationServiceConfiguration, JQueryRequestor, LocalStorageBackend, - Requestor, - StorageBackend, TokenRequestHandler, } from '@openid/appauth'; -import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth'; -import {ConfigProvider} from '../config/config.provider'; -import {SCAuthorizationProvider} from '@openstapps/core'; +import {AuthActionBuilder, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth'; import {getClientConfig, getEndpointsConfig} from './auth.provider.methods'; import {Injectable} from '@angular/core'; import {AuthService} from './auth.service'; +import {ConfigProvider} from '../config/config.provider'; +import {BeforeAppInit} from '../../before-app-init'; const TOKEN_RESPONSE_KEY = 'token_response'; @Injectable({ providedIn: 'root', }) -export class DefaultAuthService extends AuthService { +export class DefaultAuthService extends AuthService implements BeforeAppInit { public localConfiguration: AuthorizationServiceConfiguration; protected tokenHandler: TokenRequestHandler; @@ -45,13 +42,17 @@ export class DefaultAuthService extends AuthService { protected endSessionHandler: EndSessionHandler; - constructor( - protected browser: Browser = new DefaultBrowser(), - protected storage: StorageBackend = new LocalStorageBackend(), - protected requestor: Requestor = new JQueryRequestor(), - private readonly configProvider: ConfigProvider, - ) { - super(browser, storage, requestor); + constructor(private config: ConfigProvider) { + super(new DefaultBrowser(), new LocalStorageBackend(), new JQueryRequestor()); + } + + async beforeAppInit() { + this.authConfig = getClientConfig('default', this.config.auth); + this.localConfiguration = new AuthorizationServiceConfiguration( + getEndpointsConfig('default', this.config.auth), + ); + + await super.init(); } get configuration(): Promise { @@ -60,22 +61,6 @@ export class DefaultAuthService extends AuthService { return Promise.resolve(this.localConfiguration); } - public async init() { - this.setupConfiguration(); - this.setupAuthorizationNotifier(); - await this.loadTokenFromStorage(); - } - - setupConfiguration() { - const authConfig = this.configProvider.getAnyValue('auth') as { - default: SCAuthorizationProvider; - }; - this.authConfig = getClientConfig('default', authConfig); - this.localConfiguration = new AuthorizationServiceConfiguration( - getEndpointsConfig('default', authConfig), - ); - } - public async signOut() { await this.revokeTokens().catch(error => { this.notifyActionListers(AuthActionBuilder.SignOutFailed(error)); diff --git a/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts b/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts index 252ab1eb..da5bf1ab 100644 --- a/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts +++ b/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import { AuthorizationError, AuthorizationRequest, @@ -47,10 +46,10 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response'; import {PAIAAuthorizationNotifier} from './paia-authorization-notifier'; import {PAIATokenResponse} from './paia-token-response'; import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action'; -import {SCAuthorizationProvider} from '@openstapps/core'; -import {ConfigProvider} from '../../config/config.provider'; import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods'; import {Injectable} from '@angular/core'; +import {ConfigProvider} from '../../config/config.provider'; +import {BeforeAppInit} from '../../../before-app-init'; const TOKEN_RESPONSE_KEY = 'paia_token_response'; const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 minutes in seconds @@ -64,10 +63,8 @@ export interface IAuthService { getValidToken(buffer?: number): Promise; } -@Injectable({ - providedIn: 'root', -}) -export class PAIAAuthService { +@Injectable({providedIn: 'root'}) +export class PAIAAuthService implements BeforeAppInit { private _authConfig?: IAuthConfig; private _authSubject: AuthSubject = new AuthSubject(); @@ -97,7 +94,7 @@ export class PAIAAuthService { protected browser: Browser = new DefaultBrowser(), protected storage: StorageBackend = new LocalStorageBackend(), protected requestor: Requestor = new JQueryRequestor(), - private readonly configProvider: ConfigProvider, + private config: ConfigProvider, ) { this.tokenHandler = new PAIATokenRequestHandler(requestor); this.userInfoHandler = new IonicUserInfoHandler(requestor); @@ -110,6 +107,16 @@ export class PAIAAuthService { this.endSessionHandler = new IonicEndSessionHandler(browser); } + async beforeAppInit() { + this.authConfig = getClientConfig('paia', this.config.auth); + this.localConfiguration = new AuthorizationServiceConfiguration( + getEndpointsConfig('paia', this.config.auth), + ); + + this.setupAuthorizationNotifier(); + await this.loadTokenFromStorage(); + } + get token$(): Observable { return this._tokenSubject.asObservable(); } @@ -147,20 +154,6 @@ export class PAIAAuthService { return Promise.resolve(this.localConfiguration); } - public async init() { - this.setupConfiguration(); - this.setupAuthorizationNotifier(); - await this.loadTokenFromStorage(); - } - - setupConfiguration() { - const authConfig = this.configProvider.getAnyValue('auth') as { - paia: SCAuthorizationProvider; - }; - this.authConfig = getClientConfig('paia', authConfig); - this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig)); - } - protected notifyActionListers(action: IPAIAAuthAction) { /* eslint-disable unicorn/no-useless-undefined */ switch (action.action) { diff --git a/frontend/app/src/app/modules/calendar/calendar.service.ts b/frontend/app/src/app/modules/calendar/calendar.service.ts index 7627d5bb..5438ca3a 100644 --- a/frontend/app/src/app/modules/calendar/calendar.service.ts +++ b/frontend/app/src/app/modules/calendar/calendar.service.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Calendar} from '@awesome-cordova-plugins/calendar/ngx'; import {Injectable} from '@angular/core'; import {ICalEvent} from './ical/ical'; @@ -29,18 +28,18 @@ const RECURRENCE_PATTERNS: Partial> day: 'daily', }; -@Injectable() +@Injectable({providedIn: 'root'}) export class CalendarService { goToDate = new Subject(); goToDateClicked = this.goToDate.asObservable(); - calendarName = 'StApps'; + get calendarName(): string { + return this.config.app.name ?? 'StApps'; + } // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor(readonly calendar: Calendar, private readonly configProvider: ConfigProvider) { - this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps'; - } + constructor(readonly calendar: Calendar, readonly config: ConfigProvider) {} async createCalendar(): Promise { await this.calendar.createCalendar({ diff --git a/frontend/app/src/app/modules/config/config.module.ts b/frontend/app/src/app/modules/config/config.module.ts deleted file mode 100644 index cd3ee9cb..00000000 --- a/frontend/app/src/app/modules/config/config.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2019 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -import {NgModule} from '@angular/core'; -import {DataModule} from '../data/data.module'; -import {StorageModule} from '../storage/storage.module'; -import {ConfigProvider} from './config.provider'; - -/** - * TODO - */ -@NgModule({ - imports: [StorageModule, DataModule], - providers: [ConfigProvider], -}) -export class ConfigModule {} diff --git a/frontend/app/src/app/modules/config/config.provider.ts b/frontend/app/src/app/modules/config/config.provider.ts index 259f1790..096f9675 100644 --- a/frontend/app/src/app/modules/config/config.provider.ts +++ b/frontend/app/src/app/modules/config/config.provider.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-types */ /* * Copyright (C) 2022 StApps * This program is free software: you can redistribute it and/or modify it @@ -14,19 +15,17 @@ */ import {Injectable} from '@angular/core'; import {Client} from '@openstapps/api'; -import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core'; -import packageInfo from '@openstapps/core/package.json'; -import {NGXLogger} from 'ngx-logger'; -import {environment} from '../../../environments/environment'; -import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; -import {StorageProvider} from '../storage/storage.provider'; import { - ConfigFetchError, - ConfigInitError, - ConfigValueNotAvailable, - SavedConfigNotAvailable, - WrongConfigVersionInStorage, -} from './errors'; + SCAppConfiguration, + SCAuthorizationProvider, + SCBackendConfiguration, + SCIndexResponse, +} from '@openstapps/core'; +import coreInfo from '@openstapps/core/package.json'; +import {environment} from '../../../environments/environment'; +import {StorageProvider} from '../storage/storage.provider'; +import {BeforeAppInit} from '../../before-app-init'; +import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; /** * Key to store config in storage module @@ -35,145 +34,55 @@ import { */ export const STORAGE_KEY_CONFIG = 'stapps.config'; -/** - * Provides configuration - */ @Injectable({ providedIn: 'root', }) -export class ConfigProvider { - /** - * Api client - */ - client: Client; +export class ConfigProvider implements SCIndexResponse, BeforeAppInit { + private client: Client; - /** - * App configuration as IndexResponse - */ - config: SCIndexResponse; + constructor(private storageProvider: StorageProvider, httpClient: StAppsWebHttpClient) { + this.client = new Client(httpClient, environment.backend_url, environment.backend_version); + } - /** - * Version of the @openstapps/core package that app is using - */ - scVersion = packageInfo.version; + async beforeAppInit() { + this.isFirstSession = !(await this.storageProvider.has(STORAGE_KEY_CONFIG)); + // Queue config update for next launch; don't block current launch + const configUpdate = this.updateConfig(); + console.log('Config update queued'); - /** - * First session indicator (config not found in storage) - */ - firstSession = true; + const config = await this.storageProvider + .get(STORAGE_KEY_CONFIG) + .then(it => it ?? configUpdate); - /** - * Constructor, initialise api client - * @param storageProvider StorageProvider to load persistent configuration - * @param swHttpClient Api client - * @param logger An angular logger - */ - constructor( - private readonly storageProvider: StorageProvider, - swHttpClient: StAppsWebHttpClient, - private readonly logger: NGXLogger, - ) { - this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version); + Object.assign(this, config); + + console.assert( + this.backend.SCVersion === coreInfo.version, + 'Wrong config version in storage.', + 'Expected:', + coreInfo.version, + 'Actual:', + this.backend.SCVersion, + ); } /** - * Fetches configuration from backend + * Updates the config from remote */ - async fetch(): Promise { - try { - return await this.client.handshake(this.scVersion); - } catch { - throw new ConfigFetchError(); - } - } - - /** - * Returns the value of an app configuration - * @param attribute requested attribute from app configuration - */ - public getValue(attribute: keyof SCAppConfiguration) { - if (this.config.app[attribute] !== undefined) { - return this.config.app[attribute]; - } - throw new ConfigValueNotAvailable(attribute); - } - - /** - * Returns a value of the configuration (not only app configuration) - * @param attribute requested attribute from the configuration - */ - public getAnyValue(attribute: keyof SCIndexResponse) { - if (this.config[attribute] !== undefined) { - return this.config[attribute]; - } - throw new ConfigValueNotAvailable(attribute); - } - - /** - * Initialises the ConfigProvider - * @throws ConfigInitError if no configuration could be loaded. - * @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion - */ - async init(): Promise { - let loadError; - let fetchError; - // load saved configuration - try { - this.config = await this.loadLocal(); - this.firstSession = false; - this.logger.log(`initialised configuration from storage`); - if (this.config.backend.SCVersion !== this.scVersion) { - loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion); - } - } catch (error) { - loadError = error; - } - // fetch remote configuration from backend - try { - const fetchedConfig: SCIndexResponse = await this.fetch(); - await this.set(fetchedConfig); - this.logger.log(`initialised configuration from remote`); - } catch (error) { - fetchError = error; - } - // check for occurred errors and throw them - if (loadError !== undefined && fetchError !== undefined) { - throw new ConfigInitError(); - } - if (loadError !== undefined) { - this.logger.warn(loadError); - } - if (fetchError !== undefined) { - this.logger.warn(fetchError); - } - } - - /** - * Returns saved configuration from StorageModule - * @throws SavedConfigNotAvailable if no configuration could be loaded - */ - async loadLocal(): Promise { - // get local configuration - if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) { - return this.storageProvider.get(STORAGE_KEY_CONFIG); - } - throw new SavedConfigNotAvailable(); - } - - /** - * Saves the configuration from the provider - * @param config configuration to save - */ - async save(config: SCIndexResponse): Promise { + async updateConfig(): Promise { + const config = await this.client.handshake(coreInfo.version); await this.storageProvider.put(STORAGE_KEY_CONFIG, config); + + console.log(`Config updated`); + + return config; } - /** - * Sets the configuration in the module and writes it into app storage - * @param config SCIndexResponse to set - */ - async set(config: SCIndexResponse): Promise { - this.config = config; - await this.save(this.config); - } + app: SCAppConfiguration; + + auth: {default?: SCAuthorizationProvider | undefined; paia?: SCAuthorizationProvider | undefined}; + + backend: SCBackendConfiguration; + + isFirstSession: boolean; } diff --git a/frontend/app/src/app/modules/config/errors.ts b/frontend/app/src/app/modules/config/errors.ts deleted file mode 100644 index f9fcd84d..00000000 --- a/frontend/app/src/app/modules/config/errors.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2022 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import {AppError} from '../../_helpers/errors'; - -/** - * Error that is thrown when fetching from backend fails - */ -export class ConfigFetchError extends AppError { - constructor() { - super('ConfigFetchError', 'App configuration could not be fetched!'); - } -} - -/** - * Error that is thrown when the ConfigProvider could be initialised - */ -export class ConfigInitError extends AppError { - constructor() { - super('ConfigInitError', 'App configuration could not be initialised!'); - } -} - -/** - * Error that is thrown when the requested config value is not available - */ -export class ConfigValueNotAvailable extends AppError { - constructor(valueKey: string) { - super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`); - } -} - -/** - * Error that is thrown when no saved config is available - */ -export class SavedConfigNotAvailable extends AppError { - constructor() { - super('SavedConfigNotAvailable', 'No saved app configuration available.'); - } -} - -/** - * Error that is thrown when the SCVersion of the saved config is not compatible with the app - */ -export class WrongConfigVersionInStorage extends AppError { - constructor(correctVersion: string, savedVersion: string) { - super( - 'WrongConfigVersionInStorage', - `The saved configs backend version ${savedVersion} ` + - `does not equal the configured backend version ${correctVersion} of the app.`, - ); - } -} diff --git a/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts b/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts index 57281fe8..06224519 100644 --- a/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts +++ b/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts @@ -30,9 +30,8 @@ export class OffersInListComponent { @Input() set offers(it: Array>) { this._offers = it; this.price = it[0].prices?.default; - this.settingsProvider.getSetting('profile', 'group').then(group => { - this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never]; - }); + const group = this.settingsProvider.getSetting('profile', 'group'); + this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never]; const availabilities = new Set(it.map(offer => offer.availability)); this.soldOut = availabilities.has('out of stock') && availabilities.size === 1; diff --git a/frontend/app/src/app/modules/data/list/search-page.component.ts b/frontend/app/src/app/modules/data/list/search-page.component.ts index adfa8bab..91553363 100644 --- a/frontend/app/src/app/modules/data/list/search-page.component.ts +++ b/frontend/app/src/app/modules/data/list/search-page.component.ts @@ -15,16 +15,9 @@ import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {Keyboard} from '@capacitor/keyboard'; -import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular'; +import {AlertController, AnimationController} from '@ionic/angular'; import {Capacitor} from '@capacitor/core'; -import { - SCFacet, - SCFeatureConfiguration, - SCSearchFilter, - SCSearchQuery, - SCSearchSort, - SCThings, -} from '@openstapps/core'; +import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core'; import {NGXLogger} from 'ngx-logger'; import {combineLatest, Subject} from 'rxjs'; import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators'; @@ -33,9 +26,9 @@ import {SettingsProvider} from '../../settings/settings.provider'; import {DataRoutingService} from '../data-routing.service'; import {DataProvider} from '../data.provider'; import {PositionService} from '../../map/position.service'; -import {ConfigProvider} from '../../config/config.provider'; import {searchPageSwitchAnimation} from './search-page-switch-animation'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {ConfigProvider} from '../../config/config.provider'; /** * SearchPageComponent queries things and shows list of things as search results and filter as context menu @@ -144,21 +137,8 @@ export class SearchPageComponent implements OnInit { destroy$ = inject(DestroyRef); - routeAnimation: AnimationBuilder; + routeAnimation = searchPageSwitchAnimation(inject(AnimationController)); - /** - * Injects the providers and creates subscriptions - * @param alertController AlertController - * @param dataProvider DataProvider - * @param contextMenuService ContextMenuService - * @param settingsProvider SettingsProvider - * @param logger An angular logger - * @param dataRoutingService DataRoutingService - * @param router Router - * @param route ActivatedRoute - * @param positionService PositionService - * @param configProvider ConfigProvider - */ constructor( protected readonly alertController: AlertController, protected dataProvider: DataProvider, @@ -169,11 +149,8 @@ export class SearchPageComponent implements OnInit { protected router: Router, private readonly route: ActivatedRoute, protected positionService: PositionService, - private readonly configProvider: ConfigProvider, - animationController: AnimationController, - ) { - this.routeAnimation = searchPageSwitchAnimation(animationController); - } + private readonly config: ConfigProvider, + ) {} /** * Fetches items with set query configuration @@ -347,8 +324,7 @@ export class SearchPageComponent implements OnInit { }); } try { - const features = this.configProvider.getValue('features') as SCFeatureConfiguration; - this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath; + this.isHebisAvailable = !!this.config.app.features.plugins?.['hebis-plugin']?.urlPath; } catch (error) { this.logger.error(error); } diff --git a/frontend/app/src/app/modules/data/rating.provider.ts b/frontend/app/src/app/modules/data/rating.provider.ts index 3841acc6..8e8814c6 100644 --- a/frontend/app/src/app/modules/data/rating.provider.ts +++ b/frontend/app/src/app/modules/data/rating.provider.ts @@ -62,10 +62,8 @@ export class RatingProvider { return new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString(); } - private get userGroup(): Promise { - return this.settingsProvider - .getSetting('profile', 'group') - .then(it => (it as SCUserGroupSetting).value as SCUserGroup); + private get userGroup(): SCUserGroup { + return (this.settingsProvider.getSetting('profile', 'group') as SCUserGroupSetting).value as SCUserGroup; } private async getStoredRatings(): Promise { diff --git a/frontend/app/src/app/modules/favorites/favorites-page.component.ts b/frontend/app/src/app/modules/favorites/favorites-page.component.ts index 167f863a..18221919 100644 --- a/frontend/app/src/app/modules/favorites/favorites-page.component.ts +++ b/frontend/app/src/app/modules/favorites/favorites-page.component.ts @@ -12,21 +12,13 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, OnInit} from '@angular/core'; -import {AlertController, AnimationController} from '@ionic/angular'; -import {ActivatedRoute, Router} from '@angular/router'; -import {NGXLogger} from 'ngx-logger'; +import {Component, inject, OnInit} from '@angular/core'; import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators'; import {combineLatest} from 'rxjs'; import {SCThingType} from '@openstapps/core'; import {FavoritesService} from './favorites.service'; -import {DataRoutingService} from '../data/data-routing.service'; import {ContextMenuService} from '../menu/context/context-menu.service'; import {SearchPageComponent} from '../data/list/search-page.component'; -import {DataProvider} from '../data/data.provider'; -import {SettingsProvider} from '../settings/settings.provider'; -import {PositionService} from '../map/position.service'; -import {ConfigProvider} from '../config/config.provider'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** @@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni showNavigation = false; - constructor( - alertController: AlertController, - dataProvider: DataProvider, - contextMenuService: ContextMenuService, - settingsProvider: SettingsProvider, - logger: NGXLogger, - dataRoutingService: DataRoutingService, - router: Router, - route: ActivatedRoute, - positionService: PositionService, - private favoritesService: FavoritesService, - configProvider: ConfigProvider, - animationController: AnimationController, - ) { - super( - alertController, - dataProvider, - contextMenuService, - settingsProvider, - logger, - dataRoutingService, - router, - route, - positionService, - configProvider, - animationController, - ); - } + favoritesService = inject(FavoritesService); ngOnInit() { super.ngOnInit(false); diff --git a/frontend/app/src/app/modules/hebis/daia-data.provider.ts b/frontend/app/src/app/modules/hebis/daia-data.provider.ts index 860ed3a6..0cb7fc07 100644 --- a/frontend/app/src/app/modules/hebis/daia-data.provider.ts +++ b/frontend/app/src/app/modules/hebis/daia-data.provider.ts @@ -16,10 +16,9 @@ import {Injectable} from '@angular/core'; import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/response'; import {StorageProvider} from '../storage/storage.provider'; import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {ConfigProvider} from '../config/config.provider'; -import {SCFeatureConfiguration} from '@openstapps/core'; import {NGXLogger} from 'ngx-logger'; import {TranslateService} from '@ngx-translate/core'; +import {ConfigProvider} from '../config/config.provider'; /** * Generated class for the DataProvider provider. @@ -44,18 +43,10 @@ export class DaiaDataProvider { clientHeaders = new HttpHeaders(); - /** - * TODO - * @param storageProvider TODO - * @param httpClient TODO - * @param configProvider TODO - * @param logger TODO - * @param translateService TODO - */ constructor( storageProvider: StorageProvider, httpClient: HttpClient, - private configProvider: ConfigProvider, + private config: ConfigProvider, private readonly logger: NGXLogger, private translateService: TranslateService, ) { @@ -67,15 +58,14 @@ export class DaiaDataProvider { async getAvailability(id: string): Promise { if (this.daiaServiceUrl === undefined) { try { - const features = this.configProvider.getValue('features') as SCFeatureConfiguration; - if (features.extern?.daia?.url) { - this.daiaServiceUrl = features.extern?.daia?.url; + if (this.config.app.features.extern?.daia?.url) { + this.daiaServiceUrl = this.config.app.features.extern?.daia?.url; } else { this.logger.error('Daia service url undefined'); return undefined; } - if (features.extern?.hebisProxy?.url) { - this.hebisProxyUrl = features.extern?.hebisProxy?.url; + if (this.config.app.features.extern?.hebisProxy?.url) { + this.hebisProxyUrl = this.config.app.features.extern?.hebisProxy?.url; } else { this.logger.error('HeBIS proxy url undefined'); return undefined; diff --git a/frontend/app/src/app/modules/library/account/library-account.service.ts b/frontend/app/src/app/modules/library/account/library-account.service.ts index f3d81d14..e3e7d148 100644 --- a/frontend/app/src/app/modules/library/account/library-account.service.ts +++ b/frontend/app/src/app/modules/library/account/library-account.service.ts @@ -12,22 +12,17 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Injectable} from '@angular/core'; import {JQueryRequestor, Requestor} from '@openid/appauth'; -import { - SCAuthorizationProviderType, - SCFeatureConfiguration, - SCFeatureConfigurationExtern, -} from '@openstapps/core'; +import {SCAuthorizationProviderType} from '@openstapps/core'; import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types'; import {HebisDataProvider} from '../../hebis/hebis-data.provider'; import {PAIATokenResponse} from '../../auth/paia/paia-token-response'; import {AuthHelperService} from '../../auth/auth-helper.service'; -import {ConfigProvider} from '../../config/config.provider'; import {TranslateService} from '@ngx-translate/core'; import {AlertController, ToastController} from '@ionic/angular'; import {HebisSearchResponse} from '../../hebis/protocol/response'; +import {ConfigProvider} from '../../config/config.provider'; @Injectable({ providedIn: 'root', @@ -36,29 +31,26 @@ export class LibraryAccountService { /** * Base url of the external service */ - baseUrl: string; + get baseUrl(): string { + return this.config.app.features.extern!['paia'].url; + } /** * Authorization provider type */ - authType: SCAuthorizationProviderType; + get authType(): SCAuthorizationProviderType { + return this.config.app.features.extern!['paia'].authProvider!; + } constructor( protected requestor: Requestor = new JQueryRequestor(), private readonly hebisDataProvider: HebisDataProvider, private readonly authHelper: AuthHelperService, - readonly configProvider: ConfigProvider, private readonly translateService: TranslateService, private readonly alertController: AlertController, private readonly toastController: ToastController, - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const config: SCFeatureConfigurationExtern = ( - configProvider.getValue('features') as SCFeatureConfiguration - ).extern!.paia; - this.baseUrl = config.url; - this.authType = config.authProvider as SCAuthorizationProviderType; - } + private readonly config: ConfigProvider, + ) {} async getProfile() { const patron = ((await this.getValidToken()) as PAIATokenResponse).patron; diff --git a/frontend/app/src/app/modules/map/map.module.ts b/frontend/app/src/app/modules/map/map.module.ts index 6d631630..25b2f1e7 100644 --- a/frontend/app/src/app/modules/map/map.module.ts +++ b/frontend/app/src/app/modules/map/map.module.ts @@ -19,9 +19,7 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet'; import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; -import {Polygon} from 'geojson'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; -import {ConfigProvider} from '../config/config.provider'; import {DataFacetsProvider} from '../data/data-facets.provider'; import {DataModule} from '../data/data.module'; import {DataProvider} from '../data/data.provider'; @@ -35,17 +33,6 @@ import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {GeoNavigationDirective} from './geo-navigation.directive'; -/** - * Initializes the default area to show in advance (before components are initialized) - * @param configProvider An instance of the ConfigProvider to read the campus polygon from - * @param mapProvider An instance of the MapProvider to set the default polygon (area to show on the map) - */ -export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) { - return async () => { - mapProvider.defaultPolygon = (await configProvider.getValue('campusPolygon')) as Polygon; - }; -} - const mapRoutes: Routes = [ {path: 'map', component: MapPageComponent}, {path: 'map/:uid', component: MapPageComponent}, diff --git a/frontend/app/src/app/modules/map/map.provider.ts b/frontend/app/src/app/modules/map/map.provider.ts index ee488dce..851c0121 100644 --- a/frontend/app/src/app/modules/map/map.provider.ts +++ b/frontend/app/src/app/modules/map/map.provider.ts @@ -21,12 +21,11 @@ import { SCThingType, SCUuid, } from '@openstapps/core'; -import {Point, Polygon} from 'geojson'; +import {Point} from 'geojson'; import {divIcon, geoJSON, LatLng, Map, marker, Marker} from 'leaflet'; import {DataProvider} from '../data/data.provider'; import {MapPosition, PositionService} from './position.service'; import {hasValidLocation} from '../data/types/place/place-types'; -import {ConfigProvider} from '../config/config.provider'; import {SCIcon} from '../../util/ion-icon/icon'; /** @@ -36,11 +35,6 @@ import {SCIcon} from '../../util/ion-icon/icon'; providedIn: 'root', }) export class MapProvider { - /** - * Area to show when the map is initialized (shown for the first time) - */ - defaultPolygon: Polygon; - /** * Provide a point marker for a leaflet map * @param point Point to get marker for @@ -111,13 +105,7 @@ export class MapProvider { clearInterval(interval); }; - constructor( - private dataProvider: DataProvider, - private positionService: PositionService, - private configProvider: ConfigProvider, - ) { - this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon; - } + constructor(private dataProvider: DataProvider, private positionService: PositionService) {} /** * Provide the specific place by its UID diff --git a/frontend/app/src/app/modules/map/page/map-page.component.ts b/frontend/app/src/app/modules/map/page/map-page.component.ts index 5b2bd14f..6529adca 100644 --- a/frontend/app/src/app/modules/map/page/map-page.component.ts +++ b/frontend/app/src/app/modules/map/page/map-page.component.ts @@ -30,6 +30,7 @@ import {Capacitor} from '@capacitor/core'; import {pauseWhen} from '../../../util/rxjs/pause-when'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {startViewTransition} from '../../../util/view-transition'; +import {ConfigProvider} from '../../config/config.provider'; /** * The main page of the map @@ -102,7 +103,7 @@ export class MapPageComponent implements OnInit { * Options of the leaflet map */ options: MapOptions = { - center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(), + center: geoJSON(inject(ConfigProvider).app.campusPolygon).getBounds().getCenter(), layers: [ tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', { attribution: '© OpenStreetMap contributors', diff --git a/frontend/app/src/app/modules/menu/navigation/navigation.component.ts b/frontend/app/src/app/modules/menu/navigation/navigation.component.ts index e10a7c83..18f23343 100644 --- a/frontend/app/src/app/modules/menu/navigation/navigation.component.ts +++ b/frontend/app/src/app/modules/menu/navigation/navigation.component.ts @@ -12,75 +12,23 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, OnInit} from '@angular/core'; -import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; -import { - SCAppConfigurationMenuCategory, - SCLanguage, - SCThingTranslator, - SCTranslations, -} from '@openstapps/core'; -import {NavigationService} from './navigation.service'; -import config from 'capacitor.config'; -import {SettingsProvider} from '../../settings/settings.provider'; +import {Component, inject} from '@angular/core'; import {BreakpointObserver} from '@angular/cdk/layout'; +import {map} from 'rxjs/operators'; +import {ConfigProvider} from '../../config/config.provider'; -/** - * Generated class for the MenuPage page. - * - * See https://ionicframework.com/docs/components/#navigation for more info on - * Ionic pages and navigation. - */ @Component({ selector: 'stapps-navigation', styleUrls: ['navigation.scss'], templateUrl: 'navigation.html', }) -export class NavigationComponent implements OnInit { - showTabbar = true; - +export class NavigationComponent { /** - * Name of the app + * TODO: What was this for??? */ - appName = config.appName; + showTabBar$ = inject(BreakpointObserver) + .observe(['(min-width: 768px)']) + .pipe(map(({matches}) => !matches)); - /** - * Possible languages to be used for translation - */ - language: keyof SCTranslations; - - /** - * Menu entries from config module - */ - menu: SCAppConfigurationMenuCategory[]; - - /** - * Core translator - */ - translator: SCThingTranslator; - - constructor( - public translateService: TranslateService, - private navigationService: NavigationService, - private settingsProvider: SettingsProvider, - private responsive: BreakpointObserver, - ) { - translateService.onLangChange.subscribe((event: LangChangeEvent) => { - this.language = event.lang as keyof SCTranslations; - this.translator = new SCThingTranslator(this.language); - }); - - this.responsive.observe(['(min-width: 768px)']).subscribe(result => { - this.showTabbar = !result.matches; - }); - } - - async ngOnInit() { - this.language = (await this.settingsProvider.getValue( - 'profile', - 'language', - )) as keyof SCTranslations; - this.translator = new SCThingTranslator(this.language); - this.menu = await this.navigationService.getMenu(); - } + constructor(readonly config: ConfigProvider) {} } diff --git a/frontend/app/src/app/modules/menu/navigation/navigation.html b/frontend/app/src/app/modules/menu/navigation/navigation.html index 965f5a00..906f431b 100644 --- a/frontend/app/src/app/modules/menu/navigation/navigation.html +++ b/frontend/app/src/app/modules/menu/navigation/navigation.html @@ -24,8 +24,8 @@ - - + + - {{ category.translations[language].title | titlecase }} + {{ 'title' | translateSimple: category | titlecase }} - {{ item.translations[language].title | titlecase }} + {{ 'title' | translateSimple: item | titlecase }} diff --git a/frontend/app/src/app/modules/menu/navigation/navigation.module.ts b/frontend/app/src/app/modules/menu/navigation/navigation.module.ts index 53879654..9bab2bf9 100644 --- a/frontend/app/src/app/modules/menu/navigation/navigation.module.ts +++ b/frontend/app/src/app/modules/menu/navigation/navigation.module.ts @@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module'; import {TranslateModule} from '@ngx-translate/core'; import {RouterModule} from '@angular/router'; import {OfflineNoticeComponent} from './offline-notice.component'; +import {ThingTranslateModule} from '../../../translation/thing-translate.module'; @NgModule({ declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent], - imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule], + imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule], exports: [TabsComponent, RootLinkDirective, NavigationComponent], }) export class NavigationModule {} diff --git a/frontend/app/src/app/modules/menu/navigation/navigation.service.ts b/frontend/app/src/app/modules/menu/navigation/navigation.service.ts deleted file mode 100644 index 4f9033d7..00000000 --- a/frontend/app/src/app/modules/menu/navigation/navigation.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2022 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import {Injectable} from '@angular/core'; -import {SCAppConfigurationMenuCategory} from '@openstapps/core'; -import {ConfigProvider} from '../../config/config.provider'; -import {NGXLogger} from 'ngx-logger'; - -@Injectable({ - providedIn: 'root', -}) -export class NavigationService { - constructor(private configProvider: ConfigProvider, private logger: NGXLogger) {} - - async getMenu() { - let menu: SCAppConfigurationMenuCategory[] = []; - try { - menu = this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[]; - } catch (error) { - this.logger.error(`error from loading menu entries: ${error}`); - } - - return menu; - } -} diff --git a/frontend/app/src/app/modules/menu/navigation/tabs.component.ts b/frontend/app/src/app/modules/menu/navigation/tabs.component.ts index cea59eb5..b4b615b9 100644 --- a/frontend/app/src/app/modules/menu/navigation/tabs.component.ts +++ b/frontend/app/src/app/modules/menu/navigation/tabs.component.ts @@ -12,93 +12,44 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {Component} from '@angular/core'; -import {NavigationEnd, Router} from '@angular/router'; -import { - SCAppConfigurationMenuCategory, - SCLanguage, - SCThingTranslator, - SCTranslations, -} from '@openstapps/core'; +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {Event, NavigationEnd, Router} from '@angular/router'; +import {SCAppConfigurationMenuCategory} from '@openstapps/core'; import {ConfigProvider} from '../../config/config.provider'; -import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; -import {NGXLogger} from 'ngx-logger'; +import {filter} from 'rxjs'; +import {map, startWith} from 'rxjs/operators'; + +/** + * Finds a tab name based on urls + */ +function findTabFromUrl(url: string, menus: SCAppConfigurationMenuCategory[]): string { + if (url === '/') { + return menus[0]?.title ?? ''; + } + return menus.find(category => url.includes(category.route))?.title ?? ''; +} + +/** + * Type guard for event + */ +function isNavigationEnd(event: Event): event is NavigationEnd { + return event instanceof NavigationEnd; +} @Component({ selector: 'stapps-navigation-tabs', - templateUrl: 'tabs.template.html', - styleUrls: ['./tabs.component.scss'], + templateUrl: 'tabs.html', + styleUrls: ['./tabs.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TabsComponent { - /** - * Possible languages to be used for translation - */ - language: keyof SCTranslations; + menus = inject(ConfigProvider).app.menus.slice(0, 5); - /** - * Menu entries from config module - */ - menu: SCAppConfigurationMenuCategory[]; + selectedTab$ = this.router.events.pipe( + filter(isNavigationEnd), + map(event => findTabFromUrl(event.url, this.menus)), + startWith(findTabFromUrl(this.router.url, this.menus)), + ); - /** - * Core translator - */ - translator: SCThingTranslator; - - /** - * Name of selected tab - */ - selectedTab: string; - - constructor( - private readonly configProvider: ConfigProvider, - public translateService: TranslateService, - private readonly logger: NGXLogger, - private readonly router: Router, - ) { - this.language = this.translateService.currentLang as keyof SCTranslations; - this.translator = new SCThingTranslator(this.language); - void this.loadMenuEntries(); - this.router.events.subscribe((event: unknown) => { - if (event instanceof NavigationEnd) { - this.selectTab(event.url); - } - }); - this.selectTab(router.url); - - translateService.onLangChange?.subscribe((event: LangChangeEvent) => { - this.language = event.lang as keyof SCTranslations; - this.translator = new SCThingTranslator(this.language); - }); - } - - /** - * Loads menu entries from configProvider - */ - async loadMenuEntries() { - try { - const menus = (await this.configProvider.getValue('menus')) as SCAppConfigurationMenuCategory[]; - - const menu = menus.slice(0, 5); - if (menu) { - this.menu = menu; - } - } catch (error) { - this.logger.error(`error from loading menu entries: ${error}`); - } - } - - /* eslint-disable @typescript-eslint/no-explicit-any */ - selectTab(url: string) { - if (!this.menu) { - return; - } - if (url === '/') { - this.selectedTab = (this.menu[0] as any)?.title ?? ''; - return; - } - const candidate = this.menu.slice(0, 5).find(category => url.includes(category.route)); - this.selectedTab = candidate?.title ?? ''; - } + constructor(private readonly router: Router) {} } diff --git a/frontend/app/src/app/modules/menu/navigation/tabs.template.html b/frontend/app/src/app/modules/menu/navigation/tabs.html similarity index 89% rename from frontend/app/src/app/modules/menu/navigation/tabs.template.html rename to frontend/app/src/app/modules/menu/navigation/tabs.html index 93fe858b..06655b28 100644 --- a/frontend/app/src/app/modules/menu/navigation/tabs.template.html +++ b/frontend/app/src/app/modules/menu/navigation/tabs.html @@ -33,19 +33,19 @@ --> - + - {{ category.translations[language].title | titlecase }} + {{ 'title' | translateSimple: category | titlecase }} diff --git a/frontend/app/src/app/modules/menu/navigation/tabs.component.scss b/frontend/app/src/app/modules/menu/navigation/tabs.scss similarity index 100% rename from frontend/app/src/app/modules/menu/navigation/tabs.component.scss rename to frontend/app/src/app/modules/menu/navigation/tabs.scss diff --git a/frontend/app/src/app/modules/settings/settings.module.ts b/frontend/app/src/app/modules/settings/settings.module.ts index 911f4978..ef30b215 100644 --- a/frontend/app/src/app/modules/settings/settings.module.ts +++ b/frontend/app/src/app/modules/settings/settings.module.ts @@ -18,9 +18,7 @@ import {FormsModule} from '@angular/forms'; import {RouterModule, Routes} from '@angular/router'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; - import {ThingTranslateModule} from '../../translation/thing-translate.module'; -import {ConfigProvider} from '../config/config.provider'; import {SettingsItemComponent} from './item/settings-item.component'; import {SettingsPageComponent} from './page/settings-page.component'; import {SettingTranslatePipe} from './setting-translate.pipe'; @@ -60,13 +58,6 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon RouterModule.forChild(settingsRoutes), UtilModule, ], - providers: [ - ScheduleSyncService, - ConfigProvider, - SettingsProvider, - CalendarService, - ScheduleProvider, - ThingTranslatePipe, - ], + providers: [ScheduleSyncService, SettingsProvider, CalendarService, ScheduleProvider, ThingTranslatePipe], }) export class SettingsModule {} diff --git a/frontend/app/src/app/modules/settings/settings.provider.ts b/frontend/app/src/app/modules/settings/settings.provider.ts index a3f52249..6f4c8d02 100644 --- a/frontend/app/src/app/modules/settings/settings.provider.ts +++ b/frontend/app/src/app/modules/settings/settings.provider.ts @@ -16,8 +16,9 @@ import {Injectable} from '@angular/core'; import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core'; import deepMerge from 'deepmerge'; import {Subject} from 'rxjs'; -import {ConfigProvider} from '../config/config.provider'; import {StorageProvider} from '../storage/storage.provider'; +import {ConfigProvider} from '../config/config.provider'; +import {BeforeAppInit} from '../../before-app-init'; export const STORAGE_KEY_SETTINGS = 'settings'; export const STORAGE_KEY_SETTINGS_SEPARATOR = '.'; @@ -89,19 +90,19 @@ export interface SettingsAction { /** * Provider for app settings */ -@Injectable() -export class SettingsProvider { +@Injectable({ + providedIn: 'root', +}) +export class SettingsProvider implements BeforeAppInit { /** * Source of settings actions */ private settingsActionSource = new Subject(); - private needsInit = true; - /** * Order of the setting categories */ - categoriesOrder: string[]; + categoriesOrder: string[] = []; /** * Settings actions observable @@ -111,7 +112,45 @@ export class SettingsProvider { /** * Cache for the imported settings */ - settingsCache: SettingsCache; + settingsCache: SettingsCache = {}; + + constructor(private storageProvider: StorageProvider, private config: ConfigProvider) {} + + async beforeAppInit() { + try { + for (const setting of this.config.app.settings) { + this.addSetting(setting); + } + + for (const category of Object.keys(this.settingsCache)) { + if (!this.categoriesOrder.includes(category)) { + this.categoriesOrder.push(category); + } + } + } catch { + this.settingsCache = {}; + } + + if (await this.storageProvider.has(STORAGE_KEY_SETTING_VALUES)) { + // get setting values from StorageProvider into settingsCache + const valuesContainer: SettingValuesContainer = await this.storageProvider.get( + STORAGE_KEY_SETTING_VALUES, + ); + // iterate through keys of categories + for (const categoryKey of Object.keys(this.settingsCache)) { + // iterate through setting keys of category + for (const settingKey of Object.keys(this.settingsCache[categoryKey].settings)) { + // if saved setting value exists set it, otherwise set to default value + this.settingsCache[categoryKey].settings[settingKey].value = + valuesContainer[categoryKey] !== undefined && + valuesContainer[categoryKey][settingKey] !== undefined + ? valuesContainer[categoryKey][settingKey] + : this.settingsCache[categoryKey].settings[settingKey].defaultValue; + } + } + await this.saveSettingValues(); + } + } /** * Return true if all given values are valid to possible values in given settingInput @@ -192,21 +231,11 @@ export class SettingsProvider { return isValueValid; } - /** - * - * @param storage TODO - * @param configProvider TODO - */ - constructor(private readonly storage: StorageProvider, private readonly configProvider: ConfigProvider) { - this.categoriesOrder = []; - this.settingsCache = {}; - } - /** * Add an Setting to the Cache if not exist and set undefined value to defaultValue * @param setting Setting with categories, defaultValue, name, input type and valid values */ - private addSetting(setting: SCSetting): void { + addSetting(setting: SCSetting): void { if (!this.categoryExists(setting.categories[0])) { this.provideCategory(setting.categories[0]); } @@ -265,9 +294,7 @@ export class SettingsProvider { /** * Returns copy of cached settings */ - public async getCache(): Promise { - await this.init(); - + public getCache(): SettingsCache { return JSON.parse(JSON.stringify(this.settingsCache)); } @@ -284,8 +311,7 @@ export class SettingsProvider { * @param name the name of requested setting * @throws Exception if setting is not provided */ - public async getSetting(category: string, name: string): Promise { - await this.init(); + public getSetting(category: string, name: string): SCSetting { if (this.settingExists(category, name)) { // return a copy of the settings return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name])); @@ -299,8 +325,7 @@ export class SettingsProvider { * @param name the name of requested setting * @throws Exception if setting is not provided */ - public async getValue(category: string, name: string): Promise { - await this.init(); + public getValue(category: string, name: string): SCSettingValue | SCSettingValues { if (this.settingExists(category, name)) { // return a copy of the settings value return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].value)); @@ -308,47 +333,6 @@ export class SettingsProvider { throw new Error(`Setting "${name}" not provided`); } - /** - * Initializes settings from config and stored values if exist - */ - public async init(): Promise { - if (!this.needsInit) return; - this.needsInit = false; - - try { - const settings: SCSetting[] = this.configProvider.getValue('settings') as SCSetting[]; - for (const setting of settings) this.addSetting(setting); - - for (const category of Object.keys(this.settingsCache)) { - if (!this.categoriesOrder.includes(category)) { - this.categoriesOrder.push(category); - } - } - } catch { - this.settingsCache = {}; - } - - if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) { - // get setting values from StorageProvider into settingsCache - const valuesContainer: SettingValuesContainer = await this.storage.get( - STORAGE_KEY_SETTING_VALUES, - ); - // iterate through keys of categories - for (const categoryKey of Object.keys(this.settingsCache)) { - // iterate through setting keys of category - for (const settingKey of Object.keys(this.settingsCache[categoryKey].settings)) { - // if saved setting value exists set it, otherwise set to default value - this.settingsCache[categoryKey].settings[settingKey].value = - valuesContainer[categoryKey] !== undefined && - valuesContainer[categoryKey][settingKey] !== undefined - ? valuesContainer[categoryKey][settingKey] - : this.settingsCache[categoryKey].settings[settingKey].defaultValue; - } - } - await this.saveSettingValues(); - } - } - /** * Adds given setting and its category if not exist * @param setting the setting to add @@ -358,12 +342,10 @@ export class SettingsProvider { } /** - * Deletes saved values and reinitialising the settings + * Delete saved values and reinitialize the settings */ public async reset(): Promise { - await this.storage.put(STORAGE_KEY_SETTING_VALUES, {}); - this.needsInit = true; - await this.init(); + await this.storageProvider.put(STORAGE_KEY_SETTING_VALUES, {}); } /** @@ -383,15 +365,14 @@ export class SettingsProvider { * Saves cached settings in app storage */ public async saveSettingValues(): Promise { - if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) { - const savedSettingsValues: SettingValuesContainer = await this.storage.get( - STORAGE_KEY_SETTING_VALUES, - ); + if (await this.storageProvider.has(STORAGE_KEY_SETTING_VALUES)) { + const savedSettingsValues: SettingValuesContainer = + await this.storageProvider.get(STORAGE_KEY_SETTING_VALUES); const cacheSettingsValues = this.getSettingValuesFromCache(); const mergedSettingValues = deepMerge(savedSettingsValues, cacheSettingsValues); - await this.storage.put(STORAGE_KEY_SETTING_VALUES, mergedSettingValues); + await this.storageProvider.put(STORAGE_KEY_SETTING_VALUES, mergedSettingValues); } else { - await this.storage.put( + await this.storageProvider.put( STORAGE_KEY_SETTING_VALUES, this.getSettingValuesFromCache(), ); @@ -418,7 +399,6 @@ export class SettingsProvider { name: string, value: SCSettingValue | SCSettingValues, ): Promise { - await this.init(); if (this.settingExists(category, name)) { const setting: SCSetting = this.settingsCache[category].settings[name]; const isValueValid = SettingsProvider.validateValue(setting, value); diff --git a/frontend/app/src/app/modules/storage/storage.provider.ts b/frontend/app/src/app/modules/storage/storage.provider.ts index 324886b1..8e4f9592 100644 --- a/frontend/app/src/app/modules/storage/storage.provider.ts +++ b/frontend/app/src/app/modules/storage/storage.provider.ts @@ -14,17 +14,22 @@ */ import {Injectable} from '@angular/core'; import {Storage} from '@ionic/storage-angular'; +import {BeforeAppInit} from '../../before-app-init'; /** * Provides interaction with the (ionic) storage on the device (in the browser) */ -@Injectable() -export class StorageProvider { +@Injectable({providedIn: 'root'}) +export class StorageProvider implements BeforeAppInit { /** * @param storage TODO */ constructor(private readonly storage: Storage) {} + async beforeAppInit() { + await this.storage.create(); + } + /** * Deletes storage entries using keys used to save them * @param keys Unique identifiers of the resources for deletion @@ -95,13 +100,6 @@ export class StorageProvider { return (await this.storage.keys()).includes(key); } - /** - * Initializes the storage (waits until it's ready) - */ - async init(): Promise { - await this.storage.create(); - } - /** * Provides information if storage is empty or not */ diff --git a/frontend/app/src/app/translation/common-string-pipes.ts b/frontend/app/src/app/translation/common-string-pipes.ts index 49a1f346..14ae3940 100644 --- a/frontend/app/src/app/translation/common-string-pipes.ts +++ b/frontend/app/src/app/translation/common-string-pipes.ts @@ -16,7 +16,6 @@ import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import moment from 'moment'; import {Subscription} from 'rxjs'; -import {logger} from '../_helpers/ts-logger'; @Injectable() @Pipe({ @@ -163,7 +162,7 @@ export class DurationLocalizedPipe implements PipeTransform, OnDestroy { updateValue(value: string | unknown, isFrequency = false): void { if (typeof value !== 'string') { - logger.warn(`durationLocalized pipe unable to parse input: ${value}`); + console.warn('durationLocalized pipe unable to parse input', value); return; } @@ -223,7 +222,7 @@ export class MetersLocalizedPipe implements PipeTransform, OnDestroy { updateValue(value: string | number | unknown) { if (typeof value !== 'string' && typeof value !== 'number') { - logger.warn(`metersLocalized pipe unable to parse input: ${value}`); + console.warn('metersLocalized pipe unable to parse input:', value); return; } @@ -321,7 +320,7 @@ export class NumberLocalizedPipe implements PipeTransform, OnDestroy { updateValue(value: string | number | unknown, formatOptions?: string): void { if (typeof value !== 'string' && typeof value !== 'number') { - logger.warn(`numberLocalized pipe unable to parse input: ${value}`); + console.warn('numberLocalized pipe unable to parse input:', value); return; } @@ -387,7 +386,7 @@ export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy { updateValue(value: string | Date | unknown, formatOptions?: string): void { if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') { - logger.warn(`dateFormat pipe unable to parse input: ${value}`); + console.warn('dateFormat pipe unable to parse input:', value); return; } diff --git a/frontend/app/src/app/translation/translate-service-wrapper.ts b/frontend/app/src/app/translation/translate-service-wrapper.ts new file mode 100644 index 00000000..c208fe62 --- /dev/null +++ b/frontend/app/src/app/translation/translate-service-wrapper.ts @@ -0,0 +1,38 @@ +import {SCLanguageCode, SCSettingValue} from '@openstapps/core'; +import moment from 'moment/moment'; +import {getDateFnsLocale} from './dfns-locale'; +import {setDefaultOptions} from 'date-fns'; +import {SettingsProvider} from '../modules/settings/settings.provider'; +import {TranslateService} from '@ngx-translate/core'; +import {DateFnsConfigurationService} from 'ngx-date-fns'; +import {ConfigProvider} from '../modules/config/config.provider'; +import {BeforeAppInit} from '../before-app-init'; +import {inject, Injectable} from '@angular/core'; + +@Injectable({providedIn: 'root'}) +export class TranslateServiceWrapper extends TranslateService implements BeforeAppInit { + private config = inject(ConfigProvider); + + private settingsProvider = inject(SettingsProvider); + + private dateFnsConfigurationService = inject(DateFnsConfigurationService); + + async beforeAppInit() { + if (this.config.isFirstSession) { + // set language from browser + await this.settingsProvider.setSettingValue( + 'profile', + 'language', + this.getBrowserLang() as SCSettingValue, + ); + } + const languageCode = this.settingsProvider.getValue('profile', 'language') as string; + // this language will be used as a fallback when a translation isn't found in the current language + this.setDefaultLang('en'); + this.use(languageCode); + moment.locale(languageCode); + const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode); + setDefaultOptions({locale: dateFnsLocale}); + this.dateFnsConfigurationService.setLocale(dateFnsLocale); + } +} diff --git a/frontend/app/src/app/util/internet-connection.service.ts b/frontend/app/src/app/util/internet-connection.service.ts index e8ed9ea8..4c79b3e8 100644 --- a/frontend/app/src/app/util/internet-connection.service.ts +++ b/frontend/app/src/app/util/internet-connection.service.ts @@ -21,6 +21,7 @@ import { race, RetryConfig, share, + skip, Subject, takeUntil, } from 'rxjs'; @@ -61,7 +62,7 @@ export class InternetConnectionService { private doRetry(error: unknown, retryCount: number): ObservableInput { return race( this.offline$.pipe( - tap(it => console.log(it)), + skip(1), filter(it => !it), take(1), delay(Math.min(retryCount ** 4 + 100, 10_000)), diff --git a/frontend/app/src/environments/environment.ts b/frontend/app/src/environments/environment.ts index 5c13bd28..94eb87ff 100644 --- a/frontend/app/src/environments/environment.ts +++ b/frontend/app/src/environments/environment.ts @@ -21,7 +21,7 @@ export const environment = { backend_url: 'https://mobile.server.uni-frankfurt.de', app_host: 'mobile.app.uni-frankfurt.de', custom_url_scheme: 'de.anyschool.app', - backend_version: '4.0.0', + backend_version: '0.0.0', production: false, };