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 bccd90c01..37fc46139 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/theme.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 ca7772a0c..6a54802d5 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 } } @@ -6422,9 +6393,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 +7145,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 +7165,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 +7180,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 +7193,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" @@ -7232,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", @@ -7308,27 +7285,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..8b72e926d 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", @@ -71,26 +71,27 @@ "@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", + "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", @@ -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", @@ -118,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", @@ -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": {} 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/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/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/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/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/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/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/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/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/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/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/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/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 }); } /** 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. 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); + }, + ); + } + +} diff --git a/src/testing/setup.ts b/src/testing/setup.ts index 0a2d9049f..d181c5f93 100644 --- a/src/testing/setup.ts +++ b/src/testing/setup.ts @@ -12,13 +12,9 @@ // 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'), +// eslint-disable-next-line no-console +console.debug = () => { + // Silence. }; 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. };