Merge pull request #2669 from NoelDeMartin/MOBILE-3689

Mobile 3689
main
Dani Palou 2021-02-04 14:46:07 +01:00 committed by GitHub
commit ec29493a33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 575 additions and 609 deletions

5
.gitignore vendored
View File

@ -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

View File

@ -5,4 +5,3 @@ script:
- npm run lint
- npm run test:ci
- npm run build:prod

View File

@ -1,8 +1,8 @@
{
"files.associations": {
"config.json": "jsonc",
"config.*.json": "jsonc",
"moodle.config.json": "jsonc",
"moodle.*.config.json": "jsonc",
},
}

View File

@ -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"
]
}
},

View File

@ -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;
};

View File

@ -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;

View File

@ -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'));
});

View File

@ -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.
*/
{

139
package-lock.json generated
View File

@ -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",

View File

@ -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": {}

View File

@ -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());

View File

@ -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<Routes[]>(APP_ROUTES, []));
const appRoutes = CoreArray.flatten(injector.get<Routes[]>(APP_ROUTES, []));
return appRoutes.map(route => {
route.canLoad = route.canLoad ?? [];
route.canActivate = route.canActivate ?? [];
route.canLoad.push(CoreRedirectGuard);
route.canActivate.push(CoreRedirectGuard);
return route;
});
}
/**

View File

@ -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<RenderConfig>;
beforeEach(() => {
mockSingleton(CoreApp, { setStatusBarColor: jest.fn() });
@ -36,23 +35,18 @@ describe('AppComponent', () => {
mockSingleton(NgZone, { run: jest.fn() });
navigator = mockSingleton(CoreNavigator, ['navigate']);
langProvider = mock<CoreLangProvider>(['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');
});

View File

@ -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);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1087,7 +1087,7 @@ export class SQLiteDB {
}
export type SQLiteDBRecordValues = {
[key in string ]: SQLiteDBRecordValue | undefined | null;
[key: string]: SQLiteDBRecordValue | undefined | null;
};
export type SQLiteDBQueryParams = {

View File

@ -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<string, string>;
wsservice: string;
wsextservice: string;
demo_sites: Record<string, CoreSitesDemoSiteData>;
font_sizes: number[];
customurlscheme: string;
siteurl: string;
sitename: string;
multisitesdisplay: string;
sitefindersettings: Record<string, unknown>;
onlyallowlistedsites: boolean;
skipssoconfirmation: boolean;
forcedefaultlanguage: boolean;
privacypolicy: string;
notificoncolor: string;
enableanalytics: boolean;
enableonboarding: boolean;
forceColorScheme: CoreColorScheme;
forceLoginLogo: boolean;
ioswebviewscheme: string;
appstores: Record<string, string>;
displayqroncredentialscreen?: boolean;
displayqronsitescreen?: boolean;
forceOpenLinksIn: 'app' | 'browser';
};
type EnvironmentBuild = {
isProduction: boolean;
isTesting: boolean;
isDevelopment: boolean;
lastCommitHash: string;
compilationTime: number;
};

View File

@ -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.

View File

@ -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,
},
{

View File

@ -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<true | UrlTree> {
return this.guard();
}
/**
* @inheritdoc
*/
canLoad(): Promise<true | UrlTree> {
return this.guard();
}
/**
* Check if the user has any sites stored.
*/
private async guard(): Promise<true | UrlTree> {
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;
}
}

View File

@ -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',

View File

@ -1,7 +0,0 @@
<ion-content fullscreen="true" scrollY="false">
<div class="core-bglogo" slot="fixed">
<div class="core-center-spinner">
<ion-spinner></ion-spinner>
</div>
</div>
</ion-content>

View File

@ -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));
}
}

View File

@ -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<void> {
// 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<void> {
// 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<void> {
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 });
}
}

View File

@ -45,13 +45,7 @@ export class CoreLoginSitesPage implements OnInit {
* @return Promise resolved when done.
*/
async ngOnInit(): Promise<void> {
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) => {

View File

@ -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<void> {
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 })];
}
/**

View File

@ -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 });
});
});

View File

@ -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<true | UrlTree> {
return this.guard();
}
/**
* @inheritdoc
*/
canLoad(): Promise<true | UrlTree> {
return this.guard();
}
/**
* Check if the current user should be redirected to the authentication page.
*/
private async guard(): Promise<true | UrlTree> {
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;
}
}

View File

@ -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],
},
];

View File

@ -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,
},
],

View File

@ -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<true | UrlTree> {
return this.guard();
}
/**
* @inheritdoc
*/
canActivate(): Promise<true | UrlTree> {
return this.guard();
}
/**
* Check if there is a pending redirect and trigger it.
*/
private async guard(): Promise<true | UrlTree> {
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();
}
}
}

View File

@ -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();
}

View File

@ -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<SchemaVersionsManager>;
@ -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.
}
}

View File

@ -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://');
}
/**

View File

@ -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:';

View File

@ -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.

View File

@ -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<CoreSiteSchema[]>('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 });
}
/**

View File

@ -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();

View File

@ -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');
}
/**

View File

@ -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.

View File

@ -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<T> = EventEmitter<T> | Observable<T>;
/**
* 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<T>(subscribable: Subscribable<T>, 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);
},
);
}
}

View File

@ -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.
};

62
src/types/global.d.ts vendored
View File

@ -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<string, string>;
wsservice: string;
wsextservice: string;
demo_sites: Record<string, CoreSitesDemoSiteData>;
font_sizes: number[];
customurlscheme: string;
siteurl: string;
sitename: string;
multisitesdisplay: string;
sitefindersettings: Record<string, unknown>;
onlyallowlistedsites: boolean;
skipssoconfirmation: boolean;
forcedefaultlanguage: boolean;
privacypolicy: string;
notificoncolor: string;
enableanalytics: boolean;
enableonboarding: boolean;
forceColorScheme: CoreColorScheme;
forceLoginLogo: boolean;
ioswebviewscheme: string;
appstores: Record<string, string>;
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.
};