diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 937bc5166..61b44dd53 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -26,6 +26,7 @@ import { } from '@angular/router'; import { CoreArray } from '@singletons/array'; +import { CoreRedirectGuard } from '@guards/redirect'; /** * Build app routes. @@ -34,7 +35,16 @@ import { CoreArray } from '@singletons/array'; * @return App routes. */ function buildAppRoutes(injector: Injector): Routes { - return CoreArray.flatten(injector.get(APP_ROUTES, [])); + const appRoutes = CoreArray.flatten(injector.get(APP_ROUTES, [])); + + return appRoutes.map(route => { + route.canLoad = route.canLoad ?? []; + route.canActivate = route.canActivate ?? []; + route.canLoad.push(CoreRedirectGuard); + route.canActivate.push(CoreRedirectGuard); + + return route; + }); } /** diff --git a/src/app/app.component.test.ts b/src/app/app.component.test.ts index 88a874411..0d8dc9805 100644 --- a/src/app/app.component.test.ts +++ b/src/app/app.component.test.ts @@ -17,17 +17,16 @@ import { Observable } from 'rxjs'; import { AppComponent } from '@/app/app.component'; import { CoreApp } from '@services/app'; import { CoreEvents } from '@singletons/events'; -import { CoreLangProvider } from '@services/lang'; +import { CoreLang, CoreLangProvider } from '@services/lang'; import { Network, Platform, NgZone } from '@singletons'; -import { mock, mockSingleton, renderComponent, RenderConfig } from '@/testing/utils'; +import { mockSingleton, renderComponent } from '@/testing/utils'; import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; describe('AppComponent', () => { let langProvider: CoreLangProvider; let navigator: CoreNavigatorService; - let config: Partial; beforeEach(() => { mockSingleton(CoreApp, { setStatusBarColor: jest.fn() }); @@ -36,23 +35,18 @@ describe('AppComponent', () => { mockSingleton(NgZone, { run: jest.fn() }); navigator = mockSingleton(CoreNavigator, ['navigate']); - langProvider = mock(['clearCustomStrings']); - config = { - providers: [ - { provide: CoreLangProvider, useValue: langProvider }, - ], - }; + langProvider = mockSingleton(CoreLang, ['clearCustomStrings']); }); it('should render', async () => { - const fixture = await renderComponent(AppComponent, config); + const fixture = await renderComponent(AppComponent); expect(fixture.debugElement.componentInstance).toBeTruthy(); expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy(); }); it('cleans up on logout', async () => { - const fixture = await renderComponent(AppComponent, config); + const fixture = await renderComponent(AppComponent); fixture.componentInstance.ngOnInit(); CoreEvents.trigger(CoreEvents.LOGOUT); @@ -61,6 +55,4 @@ describe('AppComponent', () => { expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); }); - it.todo('shows loading while app isn\'t ready'); - }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 68ce542f6..937985104 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; +import { IonRouterOutlet } from '@ionic/angular'; -import { CoreLangProvider } from '@services/lang'; -import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; +import { CoreLang } from '@services/lang'; +import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreEvents, CoreEventSessionExpiredData, @@ -23,23 +24,20 @@ import { CoreEventSiteData, CoreEventSiteUpdatedData, } from '@singletons/events'; -import { Network, NgZone, Platform } from '@singletons'; +import { Network, NgZone, Platform, SplashScreen } from '@singletons'; import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreNavigator } from '@services/navigator'; +import { CoreSubscriptions } from '@singletons/subscriptions'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'], }) -export class AppComponent implements OnInit { +export class AppComponent implements OnInit, AfterViewInit { - constructor( - protected langProvider: CoreLangProvider, - protected loginHelper: CoreLoginHelperProvider, - ) { - } + @ViewChild(IonRouterOutlet) outlet?: IonRouterOutlet; /** * Component being initialized. @@ -58,7 +56,7 @@ export class AppComponent implements OnInit { CoreNavigator.instance.navigate('/login/sites', { reset: true }); // Unload lang custom strings. - this.langProvider.clearCustomStrings(); + CoreLang.instance.clearCustomStrings(); // Remove version classes from body. this.removeVersionClass(); @@ -66,20 +64,20 @@ export class AppComponent implements OnInit { // Listen for session expired events. CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => { - this.loginHelper.sessionExpired(data); + CoreLoginHelper.instance.sessionExpired(data); }); // Listen for passwordchange and usernotfullysetup events to open InAppBrowser. CoreEvents.on(CoreEvents.PASSWORD_CHANGE_FORCED, (data: CoreEventSiteData) => { - this.loginHelper.passwordChangeForced(data.siteId!); + CoreLoginHelper.instance.passwordChangeForced(data.siteId!); }); CoreEvents.on(CoreEvents.USER_NOT_FULLY_SETUP, (data: CoreEventSiteData) => { - this.loginHelper.openInAppForEdit(data.siteId!, '/user/edit.php', 'core.usernotfullysetup'); + CoreLoginHelper.instance.openInAppForEdit(data.siteId!, '/user/edit.php', 'core.usernotfullysetup'); }); // Listen for sitepolicynotagreed event to accept the site policy. CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data: CoreEventSiteData) => { - this.loginHelper.sitePolicyNotAgreed(data.siteId); + CoreLoginHelper.instance.sitePolicyNotAgreed(data.siteId); }); CoreEvents.on(CoreEvents.LOGIN, async (data: CoreEventSiteData) => { @@ -119,6 +117,17 @@ export class AppComponent implements OnInit { this.onPlatformReady(); } + /** + * @inheritdoc + */ + ngAfterViewInit(): void { + if (!this.outlet) { + return; + } + + CoreSubscriptions.once(this.outlet.activateEvents, () => SplashScreen.instance.hide()); + } + /** * Async init function on platform ready. */ @@ -155,8 +164,9 @@ export class AppComponent implements OnInit { */ protected loadCustomStrings(): void { const currentSite = CoreSites.instance.getCurrentSite(); + if (currentSite) { - this.langProvider.loadCustomStringsFromSite(currentSite); + CoreLang.instance.loadCustomStringsFromSite(currentSite); } } diff --git a/src/assets/img/splash.png b/src/assets/img/splash.png deleted file mode 100644 index e7889ccf9..000000000 Binary files a/src/assets/img/splash.png and /dev/null differ diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 67b19b95c..7772bf535 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -38,6 +38,7 @@ import { CoreLinkDirective } from './link'; import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; import { CoreFilterDelegate } from '@features/filter/services/filter-delegate'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; +import { CoreSubscriptions } from '@singletons/subscriptions'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -567,12 +568,7 @@ export class CoreFormatTextDirective implements OnChanges { return Promise.resolve(); } - return new Promise((resolve): void => { - const subscription = externalImage.onLoad.subscribe(() => { - subscription.unsubscribe(); - resolve(); - }); - }); + return new Promise(resolve => CoreSubscriptions.once(externalImage.onLoad, resolve)); })); // Automatically reject the promise after 5 seconds to prevent blocking the user forever. diff --git a/src/core/features/login/guards/has-sites.ts b/src/core/features/login/guards/has-sites.ts new file mode 100644 index 000000000..869ee734c --- /dev/null +++ b/src/core/features/login/guards/has-sites.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CanActivate, CanLoad, UrlTree } from '@angular/router'; + +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { Router } from '@singletons'; + +import { CoreLoginHelper } from '../services/login-helper'; + +@Injectable({ providedIn: 'root' }) +export class CoreLoginHasSitesGuard implements CanActivate, CanLoad { + + /** + * @inheritdoc + */ + canActivate(): Promise { + return this.guard(); + } + + /** + * @inheritdoc + */ + canLoad(): Promise { + return this.guard(); + } + + /** + * Check if the user has any sites stored. + */ + private async guard(): Promise { + const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSites(), []); + + if (sites.length > 0) { + return true; + } + + const [path, params] = CoreLoginHelper.instance.getAddSiteRouteInfo(); + const route = Router.instance.parseUrl(path); + + route.queryParams = params; + + return route; + } + +} diff --git a/src/core/features/login/login-lazy.module.ts b/src/core/features/login/login-lazy.module.ts index ae934e357..35dd0e501 100644 --- a/src/core/features/login/login-lazy.module.ts +++ b/src/core/features/login/login-lazy.module.ts @@ -21,16 +21,13 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreLoginSiteHelpComponent } from './components/site-help/site-help'; import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding'; +import { CoreLoginHasSitesGuard } from './guards/has-sites'; const routes: Routes = [ { path: '', - redirectTo: 'init', pathMatch: 'full', - }, - { - path: 'init', - loadChildren: () => import('./pages/init/init.module').then( m => m.CoreLoginInitPageModule), + redirectTo: 'sites', }, { path: 'site', @@ -43,6 +40,8 @@ const routes: Routes = [ { path: 'sites', loadChildren: () => import('./pages/sites/sites.module').then( m => m.CoreLoginSitesPageModule), + canLoad: [CoreLoginHasSitesGuard], + canActivate: [CoreLoginHasSitesGuard], }, { path: 'forgottenpassword', diff --git a/src/core/features/login/pages/init/init.html b/src/core/features/login/pages/init/init.html deleted file mode 100644 index 7c1b76100..000000000 --- a/src/core/features/login/pages/init/init.html +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/core/features/login/pages/init/init.scss b/src/core/features/login/pages/init/init.scss deleted file mode 100644 index 688408424..000000000 --- a/src/core/features/login/pages/init/init.scss +++ /dev/null @@ -1,25 +0,0 @@ -ion-content::part(background) { - --background: var(--core-splash-screen-background, #ffffff); - - background-image: url("~@/assets/img/splash.png"); - background-repeat: no-repeat; - background-size: 100%; - background-size: var(--core-splash-bgsize, 100vmax); - background-position: center; -} - -.core-bglogo { - display: table; - width: 100%; - height: 100%; - - .core-center-spinner { - display: table-cell; - vertical-align: middle; - text-align: center; - } - - ion-spinner { - --color: var(--core-splash-spinner-color, var(--core-color)); - } -} diff --git a/src/core/features/login/pages/init/init.ts b/src/core/features/login/pages/init/init.ts deleted file mode 100644 index 98bd7d68a..000000000 --- a/src/core/features/login/pages/init/init.ts +++ /dev/null @@ -1,125 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Component, OnInit } from '@angular/core'; - -import { CoreApp, CoreRedirectData } from '@services/app'; -import { ApplicationInit, SplashScreen } from '@singletons'; -import { CoreConstants } from '@/core/constants'; -import { CoreSites } from '@services/sites'; -import { CoreLoginHelper } from '@features/login/services/login-helper'; -import { CoreNavigator } from '@services/navigator'; - -/** - * Page that displays a "splash screen" while the app is being initialized. - */ -@Component({ - selector: 'page-core-login-init', - templateUrl: 'init.html', - styleUrls: ['init.scss'], -}) -export class CoreLoginInitPage implements OnInit { - - // @todo this page should be removed in favor of native splash - // or a splash component rendered in the root app component - - /** - * Initialize the component. - */ - async ngOnInit(): Promise { - // Wait for the app to be ready. - await ApplicationInit.instance.donePromise; - - // Check if there was a pending redirect. - const redirectData = CoreApp.instance.getRedirect(); - - if (redirectData.siteId) { - await this.handleRedirect(redirectData); - } else { - await this.loadPage(); - } - - // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen. - setTimeout(() => { - SplashScreen.instance.hide(); - }, 100); - } - - /** - * Treat redirect data. - * - * @param redirectData Redirect data. - */ - protected async handleRedirect(redirectData: CoreRedirectData): Promise { - // Unset redirect data. - CoreApp.instance.storeRedirect('', '', {}); - - // Only accept the redirect if it was stored less than 20 seconds ago. - if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) { - if (redirectData.siteId != CoreConstants.NO_SITE_ID) { - // The redirect is pointing to a site, load it. - try { - const loggedIn = await CoreSites.instance.loadSite( - redirectData.siteId!, - redirectData.page, - redirectData.params, - ); - - if (!loggedIn) { - return; - } - - await CoreNavigator.instance.navigateToSiteHome({ - params: { - redirectPath: redirectData.page, - redirectParams: redirectData.params, - }, - }); - - return; - } catch (error) { - // Site doesn't exist. - return this.loadPage(); - } - } else if (redirectData.page) { - // No site to load, open the page. - // @todo return CoreNavigator.instance.goToNoSitePage(redirectData.page, redirectData.params); - } - } - - return this.loadPage(); - } - - /** - * Load the right page. - * - * @return Promise resolved when done. - */ - protected async loadPage(): Promise { - if (CoreSites.instance.isLoggedIn()) { - if (CoreLoginHelper.instance.isSiteLoggedOut()) { - await CoreSites.instance.logout(); - - return this.loadPage(); - } - - await CoreNavigator.instance.navigateToSiteHome(); - - return; - } - - await CoreNavigator.instance.navigate('/login/sites', { reset: true }); - } - -} diff --git a/src/core/features/login/pages/sites/sites.ts b/src/core/features/login/pages/sites/sites.ts index 429682697..a85c16a7f 100644 --- a/src/core/features/login/pages/sites/sites.ts +++ b/src/core/features/login/pages/sites/sites.ts @@ -45,13 +45,7 @@ export class CoreLoginSitesPage implements OnInit { * @return Promise resolved when done. */ async ngOnInit(): Promise { - const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites()); - - if (!sites || sites.length == 0) { - CoreLoginHelper.instance.goToAddSite(true); - - return; - } + const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites(), [] as CoreSiteBasicInfo[]); // Remove protocol from the url to show more url text. this.sites = sites.map((site) => { diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 5bcf17d22..d8dbf4db6 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -34,6 +34,7 @@ import { makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { CoreNavigator } from '@services/navigator'; +import { CoreObject } from '@singletons/object'; /** * Helper provider that provides some common features regarding authentication. @@ -408,22 +409,27 @@ export class CoreLoginHelperProvider { * @return Promise resolved when done. */ async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise { - let pageRoute: string; - let params: Params; + const [path, params] = this.getAddSiteRouteInfo(showKeyboard); + await CoreNavigator.instance.navigate(path, { params, reset: setRoot }); + } + + /** + * Get path and params to visit the route to add site. + * + * @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set. + * @return Path and params. + */ + getAddSiteRouteInfo(showKeyboard?: boolean): [string, Params] { if (this.isFixedUrlSet()) { // Fixed URL is set, go to credentials page. const fixedSites = this.getFixedSites(); const url = typeof fixedSites == 'string' ? fixedSites : fixedSites[0].url; - pageRoute = '/login/credentials'; - params = { siteUrl: url }; - } else { - pageRoute = '/login/site'; - params = { showKeyboard: showKeyboard }; + return ['/login/credentials', { siteUrl: url }]; } - await CoreNavigator.instance.navigate(pageRoute, { params, reset: setRoot }); + return ['/login/site', CoreObject.withoutEmpty({ showKeyboard: showKeyboard })]; } /** diff --git a/src/core/features/login/tests/pages/init.test.ts b/src/core/features/login/tests/pages/init.test.ts deleted file mode 100644 index 8124299a8..000000000 --- a/src/core/features/login/tests/pages/init.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { CoreApp } from '@services/app'; -import { CoreLoginInitPage } from '@features/login/pages/init/init'; -import { CoreSites } from '@services/sites'; -import { ApplicationInit, SplashScreen } from '@singletons'; - -import { mockSingleton, renderComponent } from '@/testing/utils'; -import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; - -describe('CoreLoginInitPage', () => { - - let navigator: CoreNavigatorService; - - beforeEach(() => { - mockSingleton(CoreApp, { getRedirect: () => ({}) }); - mockSingleton(ApplicationInit, { donePromise: Promise.resolve() }); - mockSingleton(CoreSites, { isLoggedIn: () => false }); - mockSingleton(SplashScreen, ['hide']); - - navigator = mockSingleton(CoreNavigator, ['navigate']); - }); - - it('should render', async () => { - const fixture = await renderComponent(CoreLoginInitPage, {}); - - expect(fixture.debugElement.componentInstance).toBeTruthy(); - expect(fixture.nativeElement.querySelector('ion-spinner')).toBeTruthy(); - }); - - it('navigates to sites page after loading', async () => { - const fixture = await renderComponent(CoreLoginInitPage, {}); - - fixture.componentInstance.ngOnInit(); - await ApplicationInit.instance.donePromise; - - expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); - }); - -}); diff --git a/src/core/guards/auth.ts b/src/core/features/mainmenu/guards/auth.ts similarity index 56% rename from src/core/guards/auth.ts rename to src/core/features/mainmenu/guards/auth.ts index 3c90cfff4..5da99c25d 100644 --- a/src/core/guards/auth.ts +++ b/src/core/features/mainmenu/guards/auth.ts @@ -13,28 +13,44 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { Router, CanLoad, CanActivate, UrlTree } from '@angular/router'; +import { CanLoad, CanActivate, UrlTree } from '@angular/router'; +import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSites } from '@services/sites'; -import { ApplicationInit } from '@singletons'; +import { Router } from '@singletons'; @Injectable({ providedIn: 'root' }) -export class AuthGuard implements CanLoad, CanActivate { - - constructor(private router: Router) {} +export class CoreMainMenuAuthGuard implements CanLoad, CanActivate { + /** + * @inheritdoc + */ canActivate(): Promise { return this.guard(); } + /** + * @inheritdoc + */ canLoad(): Promise { return this.guard(); } + /** + * Check if the current user should be redirected to the authentication page. + */ private async guard(): Promise { - await ApplicationInit.instance.donePromise; + if (!CoreSites.instance.isLoggedIn()) { + return Router.instance.parseUrl('/login'); + } - return CoreSites.instance.isLoggedIn() || this.router.parseUrl('/login'); + if (CoreLoginHelper.instance.isSiteLoggedOut()) { + await CoreSites.instance.logout(); + + return Router.instance.parseUrl('/login'); + } + + return true; } } diff --git a/src/core/features/mainmenu/mainmenu.module.ts b/src/core/features/mainmenu/mainmenu.module.ts index 80842b4ed..2cfa7f26f 100644 --- a/src/core/features/mainmenu/mainmenu.module.ts +++ b/src/core/features/mainmenu/mainmenu.module.ts @@ -14,7 +14,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { Routes } from '@angular/router'; -import { AuthGuard } from '@guards/auth'; +import { CoreMainMenuAuthGuard } from '@features/mainmenu/guards/auth'; import { AppRoutingModule } from '@/app/app-routing.module'; @@ -30,8 +30,8 @@ const appRoutes: Routes = [ { path: 'main', loadChildren: () => import('./mainmenu-lazy.module').then(m => m.CoreMainMenuLazyModule), - canActivate: [AuthGuard], - canLoad: [AuthGuard], + canActivate: [CoreMainMenuAuthGuard], + canLoad: [CoreMainMenuAuthGuard], }, ]; diff --git a/src/core/guards/redirect.ts b/src/core/guards/redirect.ts new file mode 100644 index 000000000..fd01f1317 --- /dev/null +++ b/src/core/guards/redirect.ts @@ -0,0 +1,92 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CanActivate, CanLoad, UrlTree } from '@angular/router'; +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { Router } from '@singletons'; +import { CoreObject } from '@singletons/object'; +import { CoreConstants } from '../constants'; + +@Injectable({ providedIn: 'root' }) +export class CoreRedirectGuard implements CanLoad, CanActivate { + + /** + * @inheritdoc + */ + canLoad(): Promise { + return this.guard(); + } + + /** + * @inheritdoc + */ + canActivate(): Promise { + return this.guard(); + } + + /** + * Check if there is a pending redirect and trigger it. + */ + private async guard(): Promise { + const redirect = CoreApp.instance.getRedirect(); + + if (!redirect) { + return true; + } + + try { + // Only accept the redirect if it was stored less than 20 seconds ago. + if (!redirect.timemodified || Date.now() - redirect.timemodified < 20000) { + return true; + } + + // Redirect to site path. + if (redirect.siteId && redirect.siteId !== CoreConstants.NO_SITE_ID) { + const loggedIn = await CoreSites.instance.loadSite( + redirect.siteId, + redirect.page, + redirect.params, + ); + const route = Router.instance.parseUrl('/main'); + + route.queryParams = CoreObject.withoutEmpty({ + redirectPath: redirect.page, + redirectParams: redirect.params, + }); + + return loggedIn ? route : true; + } + + // Abort redirect. + if (!redirect.page) { + return true; + } + + // Redirect to non-site path. + const route = Router.instance.parseUrl(redirect.page); + + route.queryParams = CoreObject.withoutEmpty({ + redirectPath: redirect.page, + redirectParams: redirect.params, + }); + + return route; + } finally { + CoreApp.instance.forgetRedirect(); + } + } + +} diff --git a/src/core/features/login/pages/init/init.module.ts b/src/core/initializers/consume-storage-redirect.ts similarity index 54% rename from src/core/features/login/pages/init/init.module.ts rename to src/core/initializers/consume-storage-redirect.ts index b3ec8027f..d44c3efa6 100644 --- a/src/core/features/login/pages/init/init.module.ts +++ b/src/core/initializers/consume-storage-redirect.ts @@ -12,27 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { IonicModule } from '@ionic/angular'; +import { CoreApp } from '@services/app'; -import { CoreLoginInitPage } from './init'; - -const routes: Routes = [ - { - path: '', - component: CoreLoginInitPage, - }, -]; - -@NgModule({ - imports: [ - RouterModule.forChild(routes), - IonicModule, - ], - declarations: [ - CoreLoginInitPage, - ], - exports: [RouterModule], -}) -export class CoreLoginInitPageModule {} +export default function(): void { + CoreApp.instance.consumeStorageRedirect(); +} diff --git a/src/core/services/app.ts b/src/core/services/app.ts index e09f53260..6ba239381 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -25,6 +25,7 @@ import { makeSingleton, Keyboard, Network, StatusBar, Platform, Device } from '@ import { CoreLogger } from '@singletons/logger'; import { CoreColors } from '@singletons/colors'; import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; +import { CoreObject } from '@singletons/object'; /** * Object responsible of managing schema versions. @@ -58,6 +59,7 @@ export class CoreAppProvider { protected keyboardClosing = false; protected backActions: {callback: () => boolean; priority: number}[] = []; protected forceOffline = false; + protected redirect?: CoreRedirectData; // Variables for DB. protected schemaVersionsManager: Promise; @@ -516,32 +518,50 @@ export class CoreAppProvider { await deferred.promise; } + /** + * Read redirect data from local storage and clear it if it existed. + */ + consumeStorageRedirect(): void { + if (!localStorage?.getItem) { + return; + } + + try { + // Read data from storage. + const jsonData = localStorage.getItem('CoreRedirect'); + + if (!jsonData) { + return; + } + + // Clear storage. + localStorage.removeItem('CoreRedirect'); + + // Remember redirect data. + const data: CoreRedirectData = JSON.parse(jsonData); + + if (!CoreObject.isEmpty(data)) { + this.redirect = data; + } + } catch (error) { + this.logger.error('Error loading redirect data:', error); + } + } + + /** + * Forget redirect data. + */ + forgetRedirect(): void { + delete this.redirect; + } + /** * Retrieve redirect data. * * @return Object with siteid, state, params and timemodified. */ - getRedirect(): CoreRedirectData { - if (localStorage?.getItem) { - try { - const paramsJson = localStorage.getItem('CoreRedirectParams'); - const data: CoreRedirectData = { - siteId: localStorage.getItem('CoreRedirectSiteId') || undefined, - page: localStorage.getItem('CoreRedirectState') || undefined, - timemodified: parseInt(localStorage.getItem('CoreRedirectTime') || '0', 10), - }; - - if (paramsJson) { - data.params = JSON.parse(paramsJson); - } - - return data; - } catch (ex) { - this.logger.error('Error loading redirect data:', ex); - } - } - - return {}; + getRedirect(): CoreRedirectData | null { + return this.redirect || null; } /** @@ -552,15 +572,17 @@ export class CoreAppProvider { * @param params Page params. */ storeRedirect(siteId: string, page: string, params: Params): void { - if (localStorage && localStorage.setItem) { - try { - localStorage.setItem('CoreRedirectSiteId', siteId); - localStorage.setItem('CoreRedirectState', page); - localStorage.setItem('CoreRedirectParams', JSON.stringify(params)); - localStorage.setItem('CoreRedirectTime', String(Date.now())); - } catch (ex) { - // Ignore errors. - } + try { + const redirect: CoreRedirectData = { + siteId, + page, + params, + timemodified: Date.now(), + }; + + localStorage.setItem('CoreRedirect', JSON.stringify(redirect)); + } catch (ex) { + // Ignore errors. } } diff --git a/src/core/services/lang.ts b/src/core/services/lang.ts index 6ec69686f..569ace06d 100644 --- a/src/core/services/lang.ts +++ b/src/core/services/lang.ts @@ -18,6 +18,7 @@ import { CoreConstants } from '@/core/constants'; import { LangChangeEvent } from '@ngx-translate/core'; import { CoreAppProvider } from '@services/app'; import { CoreConfig } from '@services/config'; +import { CoreSubscriptions } from '@singletons/subscriptions'; import { makeSingleton, Translate, Platform } from '@singletons'; import * as moment from 'moment'; @@ -128,44 +129,25 @@ export class CoreLangProvider { // Change the language, resolving the promise when we receive the first value. promises.push(new Promise((resolve, reject) => { - const subscription = Translate.instance.use(language).subscribe((data) => { + CoreSubscriptions.once(Translate.instance.use(language), data => { // It's a language override, load the original one first. const fallbackLang = Translate.instance.instant('core.parentlanguage'); if (fallbackLang != '' && fallbackLang != 'core.parentlanguage' && fallbackLang != language) { - const fallbackSubs = Translate.instance.use(fallbackLang).subscribe((fallbackData) => { - data = Object.assign(fallbackData, data); - resolve(data); + CoreSubscriptions.once( + Translate.instance.use(fallbackLang), + fallbackData => { + data = Object.assign(fallbackData, data); - // Data received, unsubscribe. Use a timeout because we can receive a value immediately. - setTimeout(() => { - fallbackSubs.unsubscribe(); - }); - }, () => { + resolve(data); + }, // Resolve with the original language. - resolve(data); - - // Error received, unsubscribe. Use a timeout because we can receive a value immediately. - setTimeout(() => { - fallbackSubs.unsubscribe(); - }); - }); + () => resolve(data), + ); } else { resolve(data); } - - // Data received, unsubscribe. Use a timeout because we can receive a value immediately. - setTimeout(() => { - subscription.unsubscribe(); - }); - }, (error) => { - reject(error); - - // Error received, unsubscribe. Use a timeout because we can receive a value immediately. - setTimeout(() => { - subscription.unsubscribe(); - }); - }); + }, reject); })); // Change the config. diff --git a/src/core/singletons/subscriptions.ts b/src/core/singletons/subscriptions.ts new file mode 100644 index 000000000..0c66c939f --- /dev/null +++ b/src/core/singletons/subscriptions.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { EventEmitter } from '@angular/core'; +import { Observable } from 'rxjs'; + +/** + * Subscribable object. + */ +type Subscribable = EventEmitter | Observable; + +/** + * Singleton with helpers to work with subscriptions. + */ +export class CoreSubscriptions { + + /** + * Listen once to a subscribable object. + * + * @param subscribable Subscribable to listen to. + * @param onSuccess Callback to run when the subscription is updated. + * @param onError Callback to run when the an error happens. + */ + static once(subscribable: Subscribable, onSuccess: (value: T) => unknown, onError?: (error: unknown) => unknown): void { + const subscription = subscribable.subscribe( + value => { + // Unsubscribe using a timeout because we can receive a value immediately. + setTimeout(() => subscription.unsubscribe(), 0); + + onSuccess(value); + }, + error => { + // Unsubscribe using a timeout because we can receive a value immediately. + setTimeout(() => subscription.unsubscribe(), 0); + + onError?.call(error); + }, + ); + } + +}