From 95ebb007032e26e832dd30e4a14f17acf1c62c2a Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 27 Jan 2021 13:20:38 +0100 Subject: [PATCH 1/6] MOBILE-3689 cordova: Downgrade plugins --- package-lock.json | 55 ++++++++++++++++++++++++++--------------------- package.json | 19 +++++++++------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca7772a0c..dba0c97bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6422,9 +6422,9 @@ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "com-darryncampbell-cordova-plugin-intent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/com-darryncampbell-cordova-plugin-intent/-/com-darryncampbell-cordova-plugin-intent-2.0.0.tgz", - "integrity": "sha512-4f5BAyhpiGVsuouj2cokZCb99RA8V4O5YZnwGMliceFCu35BQcQvC0VLW55jl+xVDLhVymomnaBiHSktKKnK4w==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/com-darryncampbell-cordova-plugin-intent/-/com-darryncampbell-cordova-plugin-intent-1.3.0.tgz", + "integrity": "sha512-JXslndd4UiRHmirGZrwrHZHczoZ5sxM7zAylm4bPX7ZDwD4FdCHhILgDA8AeaG8wc11e0A7OEAFo0Esgc0M4yA==" }, "combined-stream": { "version": "1.0.8", @@ -7174,9 +7174,9 @@ } }, "cordova-plugin-advanced-http": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-3.0.1.tgz", - "integrity": "sha512-7P3ZoSvxvYZXNYsygkxrUIw+pnzsCVvQgRsm26XhymNqqmD9yZIcF878p6wfFVQfLzf5iRHQRwgAMcrcm+cnow==" + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-2.4.1.tgz", + "integrity": "sha512-6G8MTy/d02jE6n3Y9CVyCtD5hZGiBb+/dR2AIzhKN1RGGz38g1D2C8yE4MqHRvnmry6k/KHQWT1MsHNXrjouXQ==" }, "cordova-plugin-badge": { "version": "0.8.8", @@ -7194,9 +7194,9 @@ "integrity": "sha512-GfAibvrPdWe/ri+h3e3xkmq5bietY6yJRBIZawYDE7w600j2mtRsxgat7siWZtjRRhJuVsVwUG6H86Hyp3WKvA==" }, "cordova-plugin-customurlscheme": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-5.0.2.tgz", - "integrity": "sha512-g139Av7iYD3xcSsCd5S6a7B7dp4GTqGYtvdhh44g4OS38+aX6XkC1lsCRmROuhLIs4fkwJqkrvxacH9H4U9Gsg==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-5.0.1.tgz", + "integrity": "sha512-Nn3+MUrEGfBSFzkC9s5izzOcmpVy8Pya5oYF+CkcdqAlsqL7EqpUan3Q0Eold4EWFisVG5jRCg0XjyxL4uHGfw==" }, "cordova-plugin-device": { "version": "2.0.3", @@ -7209,9 +7209,9 @@ "integrity": "sha512-m7cughw327CjONN/qjzsTpSesLaeybksQh420/gRuSXJX5Zt9NfgsSbqqKDon6jnQ9Mm7h7imgyO2uJ34XMBtA==" }, "cordova-plugin-file-opener2": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-3.0.5.tgz", - "integrity": "sha512-tjLHDamH5+y0bJZYVe2967L1S4R8tL4Y0rJUzJGoxsyiw3FUlrJNS199POOpzZZ6Xhlntn9a2o7+84r1dMN21A==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-3.0.4.tgz", + "integrity": "sha512-bd1aCx62X2RwpC+KUiuB7quoxL/8RnPMEJU7x38Tvs+cUGLWBvsmR9+/LqGBsSns2CIqgnJ34TW0Vazoqu7Ieg==" }, "cordova-plugin-file-transfer": { "version": "1.7.1", @@ -7222,6 +7222,11 @@ "version": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", "from": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff" }, + "cordova-plugin-globalization": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz", + "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=" + }, "cordova-plugin-inappbrowser": { "version": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#715c858975cc1cb5d140afaa7973938511d38509", "from": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle" @@ -7308,27 +7313,27 @@ } }, "cordova-sqlite-storage": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cordova-sqlite-storage/-/cordova-sqlite-storage-5.1.0.tgz", - "integrity": "sha512-UmHe9yQiYblDBToh3z91WHuD6ZgmCm3VX+1QFseYQs4WVQ3+ndj22qyGby/NV0uyCgok91gB1obLjLM+9vYJEw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cordova-sqlite-storage/-/cordova-sqlite-storage-4.0.0.tgz", + "integrity": "sha512-/n5KT3TyRAC7QRe9A4Sn7bMpdsBJ6aMmHat2PsMxFZBot45SOxbAEgfGmXtq0e7OEdVzk573sIn42bLS6lNLjQ==", "requires": { - "cordova-sqlite-storage-dependencies": "3.0.0" + "cordova-sqlite-storage-dependencies": "2.1.1" } }, "cordova-sqlite-storage-dependencies": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-3.0.0.tgz", - "integrity": "sha512-A7gV5lQZc0oPrJ/a+lsZmMZr7vYou4MXyQFOY+b/dwuCMsagLT0EsL7oY54tqzpvjtzLfh0aZGGm9i8DMAIFSA==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-2.1.1.tgz", + "integrity": "sha512-1lV5Pg1FttjBmGO8z4gxtuA4BbPKtgTfUEh1Vx4boa41inizyxaowRyTeaaqEhi5gmYAaX8sRTABm9U/XckRFg==" }, "cordova-support-google-services": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.4.1.tgz", - "integrity": "sha512-1VgF9kFCOMbzgdnsDtSKaYGmWXmeciGP8+N0wTcTkL2m6Qrs1xZ82NiYEJYXe7BjHad2d06liWThqQv7iXt5HA==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.3.2.tgz", + "integrity": "sha512-RtEWzULreUX662MFWopGhFispLiHX7gUf2GijPOC2mY2oCNuUobj2mO4tl5q7PYbOreSxq+PrSekhmS6TAAWdw==" }, "cordova.plugins.diagnostic": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/cordova.plugins.diagnostic/-/cordova.plugins.diagnostic-6.0.2.tgz", - "integrity": "sha512-X3Nd0Ume1ZWndEJRtJ+BQTuTXBJfJv9hoI3PX7T/JiMMFQ/PgMwcn2DFTb27LWa65lAvMiEakMSRWmOa3/zvNg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/cordova.plugins.diagnostic/-/cordova.plugins.diagnostic-5.0.2.tgz", + "integrity": "sha512-H59o7YxJ2/COzvg+jyTpUqX8QoDcvti9dluJ9a+pHumE8lf3meWemwCl0QFa9GH+xgVd6X1Ikj/6P3+DKWd9eg==", "requires": { "colors": "^1.1.2", "elementtree": "^0.1.6", diff --git a/package.json b/package.json index 2fac4efb2..6bef0195a 100644 --- a/package.json +++ b/package.json @@ -71,23 +71,24 @@ "@types/cordova": "0.0.34", "@types/cordova-plugin-file-transfer": "^1.6.2", "@types/dom-mediacapture-record": "^1.0.7", - "com-darryncampbell-cordova-plugin-intent": "^2.0.0", + "com-darryncampbell-cordova-plugin-intent": "^1.3.0", "cordova": "^10.0.0", "cordova-android": "^8.1.0", "cordova-android-support-gradle-release": "^3.0.1", "cordova-clipboard": "^1.3.0", "cordova-ios": "^5.1.1", "cordova-plugin-add-swift-support": "^2.0.2", - "cordova-plugin-advanced-http": "^3.0.1", + "cordova-plugin-advanced-http": "^2.4.1", "cordova-plugin-badge": "^0.8.8", "cordova-plugin-camera": "^4.1.0", "cordova-plugin-chooser": "^1.3.2", - "cordova-plugin-customurlscheme": "^5.0.2", + "cordova-plugin-customurlscheme": "^5.0.1", "cordova-plugin-device": "^2.0.3", "cordova-plugin-file": "^6.0.2", - "cordova-plugin-file-opener2": "^3.0.5", + "cordova-plugin-file-opener2": "^3.0.4", "cordova-plugin-file-transfer": "1.7.1", "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", + "cordova-plugin-globalization": "^1.11.0", "cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle", "cordova-plugin-ionic-keyboard": "2.1.3", "cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle", @@ -103,9 +104,9 @@ "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", "cordova-plugin-wkwebview-cookies": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git", "cordova-plugin-zip": "^3.1.0", - "cordova-sqlite-storage": "^5.1.0", + "cordova-sqlite-storage": "^4.0.0", "cordova-support-google-services": "^1.2.1", - "cordova.plugins.diagnostic": "^6.0.2", + "cordova.plugins.diagnostic": "^5.0.2", "es6-promise-plugin": "^4.2.2", "jszip": "^3.5.0", "moment": "^2.29.0", @@ -169,7 +170,9 @@ "ios" ], "plugins": { - "cordova-plugin-advanced-http": {}, + "cordova-plugin-advanced-http": { + "OKHTTP_VERSION": "3.10.0" + }, "cordova-clipboard": {}, "cordova-plugin-badge": {}, "cordova-plugin-camera": { @@ -219,7 +222,7 @@ "ANDROID_SUPPORT_VERSION": "27.+" }, "cordova.plugins.diagnostic": { - "ANDROIDX_VERSION": "1.+" + "ANDROID_SUPPORT_VERSION": "28.+" }, "cordova-plugin-globalization": {}, "cordova-plugin-file-transfer": {} From f2f8ae664de76ada475cf68387d2bd9b027dd9b9 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 1 Feb 2021 13:32:28 +0100 Subject: [PATCH 2/6] MOBILE-3320 sites: Fix schema name registration --- src/core/classes/sqlitedb.ts | 2 +- src/core/features/h5p/h5p.module.ts | 16 ++-------------- src/core/features/xapi/xapi.module.ts | 6 ++---- src/core/services/sites.ts | 4 ++-- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/core/classes/sqlitedb.ts b/src/core/classes/sqlitedb.ts index 5ee68de65..dda138da6 100644 --- a/src/core/classes/sqlitedb.ts +++ b/src/core/classes/sqlitedb.ts @@ -1087,7 +1087,7 @@ export class SQLiteDB { } export type SQLiteDBRecordValues = { - [key in string ]: SQLiteDBRecordValue | undefined | null; + [key: string]: SQLiteDBRecordValue | undefined | null; }; export type SQLiteDBQueryParams = { diff --git a/src/core/features/h5p/h5p.module.ts b/src/core/features/h5p/h5p.module.ts index 011fd98ad..cc9b809dc 100644 --- a/src/core/features/h5p/h5p.module.ts +++ b/src/core/features/h5p/h5p.module.ts @@ -17,13 +17,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreH5PComponentsModule } from './components/components.module'; -import { - CONTENT_TABLE_NAME, - LIBRARIES_TABLE_NAME, - LIBRARY_DEPENDENCIES_TABLE_NAME, - CONTENTS_LIBRARIES_TABLE_NAME, - LIBRARIES_CACHEDASSETS_TABLE_NAME, -} from './services/database/h5p'; +import { SITE_SCHEMA } from './services/database/h5p'; import { CoreH5PPluginFileHandler } from './services/handlers/pluginfile'; @NgModule({ @@ -33,13 +27,7 @@ import { CoreH5PPluginFileHandler } from './services/handlers/pluginfile'; providers: [ { provide: CORE_SITE_SCHEMAS, - useValue: [ - CONTENT_TABLE_NAME, - LIBRARIES_TABLE_NAME, - LIBRARY_DEPENDENCIES_TABLE_NAME, - CONTENTS_LIBRARIES_TABLE_NAME, - LIBRARIES_CACHEDASSETS_TABLE_NAME, - ], + useValue: [SITE_SCHEMA], multi: true, }, { diff --git a/src/core/features/xapi/xapi.module.ts b/src/core/features/xapi/xapi.module.ts index 8671585e0..23f76b909 100644 --- a/src/core/features/xapi/xapi.module.ts +++ b/src/core/features/xapi/xapi.module.ts @@ -15,16 +15,14 @@ import { NgModule } from '@angular/core'; import { CORE_SITE_SCHEMAS } from '@services/sites'; -import { STATEMENTS_TABLE_NAME } from './services/database/xapi'; +import { SITE_SCHEMA } from './services/database/xapi'; @NgModule({ imports: [], providers: [ { provide: CORE_SITE_SCHEMAS, - useValue: [ - STATEMENTS_TABLE_NAME, - ], + useValue: [SITE_SCHEMA], multi: true, }, ], diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index b28c44c5e..18b68b0a3 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -51,7 +51,7 @@ import { import { CoreArray } from '../singletons/array'; import { CoreNetworkError } from '@classes/errors/network-error'; -export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); +export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); /* * Service to manage and interact with sites. @@ -1576,7 +1576,7 @@ export class CoreSitesProvider { } // Set installed version. - await db.insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name, version: schema.version }); + await db.insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name: schema.name, version: schema.version }); } /** From bc1d0ee338fb79f2ff837052a39199bb5a7867eb Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 1 Feb 2021 17:27:28 +0100 Subject: [PATCH 3/6] MOBILE-3689 config: Migrate config build to gulp --- .gitignore | 5 +- .travis.yml | 1 - .vscode/settings.json | 4 +- angular.json | 15 ++-- config/webpack.config.js | 40 ---------- gulp/task-build-env.js | 41 ++++++++++ gulpfile.js | 9 ++- config/config.json => moodle.config.json | 0 ...example.json => moodle.example.config.json | 5 +- package-lock.json | 79 ++++++------------- package.json | 10 +-- config/utils.js => scripts/env-utils.js | 4 +- src/core/constants.ts | 54 ++++++++++++- src/core/services/groups.ts | 1 - src/testing/setup.ts | 9 --- src/types/global.d.ts | 62 +-------------- 16 files changed, 150 insertions(+), 189 deletions(-) delete mode 100644 config/webpack.config.js create mode 100644 gulp/task-build-env.js rename config/config.json => moodle.config.json (100%) rename config/config.example.json => moodle.example.config.json (61%) rename config/utils.js => scripts/env-utils.js (88%) diff --git a/.gitignore b/.gitignore index 3e8297b04..73624db7a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,8 @@ npm-debug.log* /plugins /www -/config/config.*.json +/moodle.*.config.json +!/moodle.example.config.json + /src/assets/lang/* +/src/assets/env.json diff --git a/.travis.yml b/.travis.yml index 3cefd498a..64da6f723 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,3 @@ script: - npm run lint - npm run test:ci - npm run build:prod - diff --git a/.vscode/settings.json b/.vscode/settings.json index cf9b6ae4c..d8b699e18 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { "files.associations": { - "config.json": "jsonc", - "config.*.json": "jsonc", + "moodle.config.json": "jsonc", + "moodle.*.config.json": "jsonc", }, } diff --git a/angular.json b/angular.json index 7caa291e1..a1ffa0962 100644 --- a/angular.json +++ b/angular.json @@ -12,7 +12,7 @@ "schematics": {}, "architect": { "build": { - "builder": "@angular-builders/custom-webpack:browser", + "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "www", "index": "src/index.html", @@ -36,10 +36,7 @@ "input": "src/theme/global.scss" } ], - "scripts": [], - "customWebpackConfig": { - "path": "./config/webpack.config.js" - } + "scripts": [] }, "configurations": { "production": { @@ -66,7 +63,7 @@ } }, "serve": { - "builder": "@angular-builders/custom-webpack:dev-server", + "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "app:build" }, @@ -89,9 +86,9 @@ "builder": "@angular-eslint/builder:lint", "options": { "lintFilePatterns": [ - "src/**/*.ts", - "src/core/**/*.html", - "src/addons/**/*.html" + "src/**/*.ts", + "src/core/**/*.html", + "src/addons/**/*.html" ] } }, diff --git a/config/webpack.config.js b/config/webpack.config.js deleted file mode 100644 index 27266089c..000000000 --- a/config/webpack.config.js +++ /dev/null @@ -1,40 +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. - -const webpack = require('webpack'); -const { getConfig, getBuild } = require('./utils'); -const { resolve } = require('path'); - -module.exports = config => { - config.resolve.alias['@'] = resolve('src'); - config.resolve.alias['@classes'] = resolve('src/core/classes'); - config.resolve.alias['@components'] = resolve('src/core/components'); - config.resolve.alias['@directives'] = resolve('src/core/directives'); - config.resolve.alias['@features'] = resolve('src/core/features'); - config.resolve.alias['@guards'] = resolve('src/core/guards'); - config.resolve.alias['@pipes'] = resolve('src/core/pipes'); - config.resolve.alias['@services'] = resolve('src/core/services'); - config.resolve.alias['@singletons'] = resolve('src/core/singletons'); - - config.plugins.push( - new webpack.DefinePlugin({ - 'window.MoodleApp': { - CONFIG: JSON.stringify(getConfig(process.env.NODE_ENV || 'development')), - BUILD: JSON.stringify(getBuild(process.env.NODE_ENV || 'development')), - }, - }), - ); - - return config; -}; diff --git a/gulp/task-build-env.js b/gulp/task-build-env.js new file mode 100644 index 000000000..b96005906 --- /dev/null +++ b/gulp/task-build-env.js @@ -0,0 +1,41 @@ +// (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. + +const { getConfig, getBuild } = require('../scripts/env-utils'); +const { resolve } = require('path'); +const { writeFile } = require('fs'); + +/** + * Task to build an env file depending on the current environment. + */ +class BuildEnvTask { + + /** + * Run the task. + * + * @param done Function to call when done. + */ + run(done) { + const envFile = resolve(__dirname, '../src/assets/env.json'); + const env = { + CONFIG: getConfig(process.env.NODE_ENV || 'development'), + BUILD: getBuild(process.env.NODE_ENV || 'development'), + }; + + writeFile(envFile, JSON.stringify(env), done); + } + +} + +module.exports = BuildEnvTask; diff --git a/gulpfile.js b/gulpfile.js index 739e799c2..2115bad2e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -13,6 +13,7 @@ // limitations under the License. const BuildLangTask = require('./gulp/task-build-lang'); +const BuildEnvTask = require('./gulp/task-build-env'); const PushTask = require('./gulp/task-push'); const Utils = require('./gulp/utils'); const gulp = require('gulp'); @@ -34,12 +35,18 @@ gulp.task('lang', (done) => { new BuildLangTask().run(paths.lang, done); }); +// Build an env file depending on the current environment. +gulp.task('env', (done) => { + new BuildEnvTask().run(done); +}); + gulp.task('push', (done) => { new PushTask().run(args, done); }); -gulp.task('default', gulp.parallel('lang')); +gulp.task('default', gulp.parallel(['lang', 'env'])); gulp.task('watch', () => { gulp.watch(paths.lang, { interval: 500 }, gulp.parallel('lang')); + gulp.watch(['./moodle.config.json', './moodle.*.config.json'], { interval: 500 }, gulp.parallel('env')); }); diff --git a/config/config.json b/moodle.config.json similarity index 100% rename from config/config.json rename to moodle.config.json diff --git a/config/config.example.json b/moodle.example.config.json similarity index 61% rename from config/config.example.json rename to moodle.example.config.json index d160714af..a94e15ea6 100644 --- a/config/config.example.json +++ b/moodle.example.config.json @@ -1,9 +1,8 @@ /** * Application config. * - * You can create your own environment files such as "config.prod.json" and "config.dev.json" - * to override some values. The values will be merged, so you don't need to duplicate everything - * in this file. + * You can create your own environment files such as "moodle.config.prod.json" and "moodle.config.dev.json" + * to override some values. The values will be merged, so you don't need to duplicate everything in this file. */ { diff --git a/package-lock.json b/package-lock.json index dba0c97bf..977dd2cfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,58 +4,29 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@angular-builders/custom-webpack": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-10.0.1.tgz", - "integrity": "sha512-YDy5zEKVwXdoXLjmbsY6kGaEbmunQxaPipxrwLUc9hIjRLU2WcrX9vopf1R9Pgj4POad73IPBNGu+ibqNRFIEQ==", - "dev": true, - "requires": { - "@angular-devkit/architect": ">=0.1000.0 < 0.1100.0", - "@angular-devkit/build-angular": ">=0.1000.0 < 0.1100.0", - "@angular-devkit/core": "^10.0.0", - "lodash": "^4.17.15", - "ts-node": "^9.0.0", - "webpack-merge": "^4.2.2" - }, - "dependencies": { - "ts-node": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", - "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", - "dev": true, - "requires": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - } - } - } - }, "@angular-devkit/architect": { - "version": "0.1001.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1001.4.tgz", - "integrity": "sha512-0U/w+61vWxnEe9Ln/hNOH6O27FVcU+s/sbJAuPREbP875R4bQzK2PX0eYRlISzkDtQyw16GzlsikLWOoJ3vjTA==", + "version": "0.1101.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1101.2.tgz", + "integrity": "sha512-MLmBfHiiyPhbFSSAX4oMecPjEuBauOui5uBpI6BKNnk/7783fznbkbAKjXlOco7M81gkNeEoHMR8c+mOfcvv7g==", "dev": true, "requires": { - "@angular-devkit/core": "10.1.4", - "rxjs": "6.6.2" + "@angular-devkit/core": "11.1.2", + "rxjs": "6.6.3" }, "dependencies": { "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true } } @@ -249,22 +220,22 @@ } }, "@angular-devkit/core": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.4.tgz", - "integrity": "sha512-B1cwVcfChBvmEacydE2uqZ1UC2ez1G+KY0GyVnCQKpAb/DdfDgtaYjTx9JLvGQjE/BlVklEj8YCKDjVV0WPE5g==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-11.1.2.tgz", + "integrity": "sha512-V7zOMqL2l56JcwXVyswkG+7+t67r9XtkrVzRcG2Z5ZYwafU+iKWMwg5kBFZr1SX7fM1M9E4MpskxqtagQeUKng==", "dev": true, "requires": { - "ajv": "6.12.4", + "ajv": "6.12.6", "fast-json-stable-stringify": "2.1.0", "magic-string": "0.25.7", - "rxjs": "6.6.2", + "rxjs": "6.6.3", "source-map": "0.7.3" }, "dependencies": { "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -274,18 +245,18 @@ } }, "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true } } diff --git a/package.json b/package.json index 6bef0195a..07da98963 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "start": "ionic serve", "build": "ionic build", "build:prod": "ionic build --prod", - "test": "jest --verbose", - "test:ci": "jest -ci --runInBand --verbose", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", + "test": "NODE_ENV=testing gulp && jest --verbose", + "test:ci": "NODE_ENV=testing gulp && jest -ci --runInBand --verbose", + "test:watch": "NODE_ENV=testing gulp watch & jest --watch", + "test:coverage": "NODE_ENV=testing gulp && jest --coverage", "lint": "ng lint", "ionic:serve:before": "gulp", "ionic:serve": "gulp watch & ng serve", @@ -119,7 +119,7 @@ "zone.js": "~0.10.3" }, "devDependencies": { - "@angular-builders/custom-webpack": "^10.0.1", + "@angular-devkit/architect": "^0.1101.2", "@angular-devkit/build-angular": "~0.1000.0", "@angular-eslint/builder": "0.5.0-beta.2", "@angular-eslint/eslint-plugin": "0.5.0-beta.2", diff --git a/config/utils.js b/scripts/env-utils.js similarity index 88% rename from config/utils.js rename to scripts/env-utils.js index e2d482c8f..39944ac00 100644 --- a/config/utils.js +++ b/scripts/env-utils.js @@ -23,9 +23,9 @@ function getConfig(environment) { development: ['dev', 'development'], production: ['prod', 'production'], }; - const config = parseJsonc(readFileSync(resolve('config/config.json')).toString()); + const config = parseJsonc(readFileSync(resolve(__dirname, '../moodle.config.json')).toString()); const envSuffixes = (envSuffixesMap[environment] || []); - const envConfigPath = envSuffixes.map(suffix => resolve(`config/config.${suffix}.json`)).find(existsSync); + const envConfigPath = envSuffixes.map(suffix => resolve(__dirname, `../moodle.${suffix}.config.json`)).find(existsSync); if (envConfigPath) { const envConfig = parseJsonc(readFileSync(envConfigPath).toString()); diff --git a/src/core/constants.ts b/src/core/constants.ts index b4bd8c42a..0f43c62e3 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -12,6 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +/* eslint-disable @typescript-eslint/naming-convention */ + +import { CoreColorScheme } from '@features/settings/services/settings-helper'; +import { CoreSitesDemoSiteData } from '@services/sites'; +import envJson from '@/assets/env.json'; + /** * Context levels enumeration. */ @@ -114,7 +120,51 @@ export class CoreConstants { static readonly MOD_ARCHETYPE_SYSTEM = 3; // System (not user-addable) module archetype. // Config & environment constants. - static readonly CONFIG = (window as unknown as MoodleAppWindow).MoodleApp.CONFIG; // Data parsed from config.json files. - static readonly BUILD = (window as unknown as MoodleAppWindow).MoodleApp.BUILD; // Environment info. + static readonly CONFIG = envJson.CONFIG as unknown as EnvironmentConfig; // Data parsed from config.json files. + static readonly BUILD = envJson.BUILD as unknown as EnvironmentBuild; // Build info. } + +type EnvironmentConfig = { + app_id: string; + appname: string; + versioncode: number; + versionname: string; + cache_update_frequency_usually: number; + cache_update_frequency_often: number; + cache_update_frequency_sometimes: number; + cache_update_frequency_rarely: number; + default_lang: string; + languages: Record; + wsservice: string; + wsextservice: string; + demo_sites: Record; + font_sizes: number[]; + customurlscheme: string; + siteurl: string; + sitename: string; + multisitesdisplay: string; + sitefindersettings: Record; + onlyallowlistedsites: boolean; + skipssoconfirmation: boolean; + forcedefaultlanguage: boolean; + privacypolicy: string; + notificoncolor: string; + enableanalytics: boolean; + enableonboarding: boolean; + forceColorScheme: CoreColorScheme; + forceLoginLogo: boolean; + ioswebviewscheme: string; + appstores: Record; + displayqroncredentialscreen?: boolean; + displayqronsitescreen?: boolean; + forceOpenLinksIn: 'app' | 'browser'; +}; + +type EnvironmentBuild = { + isProduction: boolean; + isTesting: boolean; + isDevelopment: boolean; + lastCommitHash: string; + compilationTime: number; +}; diff --git a/src/core/services/groups.ts b/src/core/services/groups.ts index f38c1efa2..39541f9df 100644 --- a/src/core/services/groups.ts +++ b/src/core/services/groups.ts @@ -19,7 +19,6 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; import { CoreWSExternalWarning } from '@services/ws'; -import { CoreCourseBase } from '@/types/global'; const ROOT_CACHE_KEY = 'mmGroups:'; diff --git a/src/testing/setup.ts b/src/testing/setup.ts index 0a2d9049f..aba69b950 100644 --- a/src/testing/setup.ts +++ b/src/testing/setup.ts @@ -12,13 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -/* eslint-disable @typescript-eslint/naming-convention */ - import 'jest-preset-angular'; - -import { getConfig, getBuild } from '../../config/utils'; - -(window as unknown as MoodleAppWindow).MoodleApp = { - CONFIG: getConfig('testing'), - BUILD: getBuild('testing'), -}; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index cbd429038..f8d2f8031 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -13,69 +13,13 @@ // limitations under the License. /* eslint-disable @typescript-eslint/naming-convention */ - -import { CoreColorScheme } from '@features/settings/services/settings-helper'; -import { CoreSitesDemoSiteData } from '@services/sites'; - -declare global { - - interface Window { - __Zone_disable_customElements: boolean; - } - - type MoodleAppWindow = { - MoodleApp: { - CONFIG: { - app_id: string; - appname: string; - versioncode: number; - versionname: string; - cache_update_frequency_usually: number; - cache_update_frequency_often: number; - cache_update_frequency_sometimes: number; - cache_update_frequency_rarely: number; - default_lang: string; - languages: Record; - wsservice: string; - wsextservice: string; - demo_sites: Record; - font_sizes: number[]; - customurlscheme: string; - siteurl: string; - sitename: string; - multisitesdisplay: string; - sitefindersettings: Record; - onlyallowlistedsites: boolean; - skipssoconfirmation: boolean; - forcedefaultlanguage: boolean; - privacypolicy: string; - notificoncolor: string; - enableanalytics: boolean; - enableonboarding: boolean; - forceColorScheme: CoreColorScheme; - forceLoginLogo: boolean; - ioswebviewscheme: string; - appstores: Record; - displayqroncredentialscreen?: boolean; - displayqronsitescreen?: boolean; - forceOpenLinksIn: 'app' | 'browser'; - }; - - BUILD: { - isProduction: boolean; - isTesting: boolean; - isDevelopment: boolean; - lastCommitHash: string; - compilationTime: number; - }; - }; - }; - +interface Window { + __Zone_disable_customElements: boolean; } /** * Course base definition. */ -export type CoreCourseBase = { +type CoreCourseBase = { id: number; // Course Id. }; From 033860d18b6ae0c2d39359477a3cbdecb4ace2d7 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 2 Feb 2021 18:13:30 +0100 Subject: [PATCH 4/6] MOBILE-3689 android: Enable ionic-webview plugin --- package-lock.json | 5 +++-- package.json | 2 +- src/core/services/file.ts | 8 +++++--- src/core/services/utils/iframe.ts | 2 +- src/core/services/utils/url.ts | 7 ++++--- src/core/services/ws.ts | 2 +- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 977dd2cfb..6a54802d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7208,8 +7208,9 @@ "integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ==" }, "cordova-plugin-ionic-webview": { - "version": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#ac90a8ac88e2c0512d6b250249b1f673f2fbcb68", - "from": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-webview/-/cordova-plugin-ionic-webview-4.2.1.tgz", + "integrity": "sha512-7KrmqLaOGq1RP8N2z1ezN1kqkWFzTwwMvQ3/qAkd+exxFZuOe3DIN4eaU1gdNphsxdirI8Ajnr9q4So5vQbWqw==" }, "cordova-plugin-local-notification": { "version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836", diff --git a/package.json b/package.json index 07da98963..8b72e926d 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "cordova-plugin-globalization": "^1.11.0", "cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle", "cordova-plugin-ionic-keyboard": "2.1.3", - "cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle", + "cordova-plugin-ionic-webview": "^4.2.1", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", "cordova-plugin-media": "^5.0.3", "cordova-plugin-media-capture": "^3.0.3", diff --git a/src/core/services/file.ts b/src/core/services/file.ts index 5f48fa2e8..539aea31e 100644 --- a/src/core/services/file.ts +++ b/src/core/services/file.ts @@ -1233,7 +1233,7 @@ export class CoreFileProvider { * @return Converted src. */ convertFileSrc(src: string): string { - return CoreApp.instance.isIOS() ? WebView.instance.convertFileSrc(src) : src; + return CoreApp.instance.isMobile() ? WebView.instance.convertFileSrc(src) : src; } /** @@ -1243,11 +1243,13 @@ export class CoreFileProvider { * @return Unconverted src. */ unconvertFileSrc(src: string): string { - if (!CoreApp.instance.isIOS()) { + if (!CoreApp.instance.isMobile()) { return src; } - return src.replace(CoreConstants.CONFIG.ioswebviewscheme + '://localhost/_app_file_', 'file://'); + const scheme = CoreApp.instance.isIOS() ? CoreConstants.CONFIG.ioswebviewscheme : 'http'; + + return src.replace(scheme + '://localhost/_app_file_', 'file://'); } /** diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index b2ee65307..85dc4cbb9 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -390,7 +390,7 @@ export class CoreIframeUtilsProvider { return; } - if (urlParts.protocol && !CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol)) { + if (urlParts.protocol && !CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain || '')) { // Scheme suggests it's an external resource. event && event.preventDefault(); diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index 428cbce48..c773dd3fb 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -424,7 +424,7 @@ export class CoreUrlUtilsProvider { isLocalFileUrl(url: string): boolean { const urlParts = CoreUrl.parse(url); - return this.isLocalFileUrlScheme(urlParts?.protocol || ''); + return this.isLocalFileUrlScheme(urlParts?.protocol || '', urlParts?.domain || ''); } /** @@ -433,7 +433,7 @@ export class CoreUrlUtilsProvider { * @param scheme Scheme to check. * @return Whether the scheme belongs to a local file. */ - isLocalFileUrlScheme(scheme: string): boolean { + isLocalFileUrlScheme(scheme: string, domain: string): boolean { if (!scheme) { return false; } @@ -442,7 +442,8 @@ export class CoreUrlUtilsProvider { return scheme == 'cdvfile' || scheme == 'file' || scheme == 'filesystem' || - scheme == CoreConstants.CONFIG.ioswebviewscheme; + scheme == CoreConstants.CONFIG.ioswebviewscheme || + (scheme === 'http' && domain === 'localhost'); } /** diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index 33b285cc8..66420bbf9 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -927,7 +927,7 @@ export class CoreWSProvider { options.responseType = options.responseType || 'json'; options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout; - if (CoreApp.instance.isIOS()) { + if (CoreApp.instance.isMobile()) { // Use the cordova plugin. if (url.indexOf('file://') === 0) { // We cannot load local files using the http native plugin. Use file provider instead. From 2a5e29b1c34d4704ee31b5d6094df447edba42ea Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 2 Feb 2021 18:41:58 +0100 Subject: [PATCH 5/6] MOBILE-3689 init: Replace /login/init with guards --- src/app/app-routing.module.ts | 12 +- src/app/app.component.test.ts | 18 +-- src/app/app.component.ts | 42 +++--- src/assets/img/splash.png | Bin 55907 -> 0 bytes src/core/directives/format-text.ts | 8 +- src/core/features/login/guards/has-sites.ts | 59 +++++++++ src/core/features/login/login-lazy.module.ts | 9 +- src/core/features/login/pages/init/init.html | 7 - src/core/features/login/pages/init/init.scss | 25 ---- src/core/features/login/pages/init/init.ts | 125 ------------------ src/core/features/login/pages/sites/sites.ts | 8 +- .../features/login/services/login-helper.ts | 22 +-- .../features/login/tests/pages/init.test.ts | 52 -------- .../{ => features/mainmenu}/guards/auth.ts | 30 ++++- src/core/features/mainmenu/mainmenu.module.ts | 6 +- src/core/guards/redirect.ts | 92 +++++++++++++ .../consume-storage-redirect.ts} | 27 +--- src/core/services/app.ts | 82 +++++++----- src/core/services/lang.ts | 40 ++---- src/core/singletons/subscriptions.ts | 52 ++++++++ 20 files changed, 359 insertions(+), 357 deletions(-) delete mode 100644 src/assets/img/splash.png create mode 100644 src/core/features/login/guards/has-sites.ts delete mode 100644 src/core/features/login/pages/init/init.html delete mode 100644 src/core/features/login/pages/init/init.scss delete mode 100644 src/core/features/login/pages/init/init.ts delete mode 100644 src/core/features/login/tests/pages/init.test.ts rename src/core/{ => features/mainmenu}/guards/auth.ts (56%) create mode 100644 src/core/guards/redirect.ts rename src/core/{features/login/pages/init/init.module.ts => initializers/consume-storage-redirect.ts} (54%) create mode 100644 src/core/singletons/subscriptions.ts 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 e7889ccf91e612b8a62d5ff911239c96d31360fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55907 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

CNLufl*b6as(quMR#({!th>U$wrya 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 { - 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); + }, + ); + } + +} From eed69045603e6de3389448d58c6a7ea030354514 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 2 Feb 2021 18:42:25 +0100 Subject: [PATCH 6/6] MOBILE-3320 tests: Silence logs in tests --- src/testing/setup.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/testing/setup.ts b/src/testing/setup.ts index aba69b950..d181c5f93 100644 --- a/src/testing/setup.ts +++ b/src/testing/setup.ts @@ -13,3 +13,8 @@ // limitations under the License. import 'jest-preset-angular'; + +// eslint-disable-next-line no-console +console.debug = () => { + // Silence. +};