commit
dd9ee9c1fc
|
@ -273,8 +273,10 @@ class behat_app extends behat_app_helper {
|
||||||
// Wait until the main page appears.
|
// Wait until the main page appears.
|
||||||
$this->spin(
|
$this->spin(
|
||||||
function($context, $args) {
|
function($context, $args) {
|
||||||
$mainmenu = $context->getSession()->getPage()->find('xpath', '//page-core-mainmenu');
|
$initialpage = $context->getSession()->getPage()->find('xpath', '//page-core-mainmenu') ??
|
||||||
if ($mainmenu) {
|
$context->getSession()->getPage()->find('xpath', '//page-core-login-change-password') ??
|
||||||
|
$context->getSession()->getPage()->find('xpath', '//page-core-user-complete-profile');
|
||||||
|
if ($initialpage) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
throw new DriverException('Moodle App main page not loaded after login');
|
throw new DriverException('Moodle App main page not loaded after login');
|
||||||
|
|
|
@ -1903,8 +1903,6 @@
|
||||||
"core.login.changepassword": "moodle",
|
"core.login.changepassword": "moodle",
|
||||||
"core.login.changepasswordbutton": "local_moodlemobileapp",
|
"core.login.changepasswordbutton": "local_moodlemobileapp",
|
||||||
"core.login.changepasswordhelp": "local_moodlemobileapp",
|
"core.login.changepasswordhelp": "local_moodlemobileapp",
|
||||||
"core.login.changepasswordinstructions": "local_moodlemobileapp",
|
|
||||||
"core.login.changepasswordlogoutinstructions": "local_moodlemobileapp",
|
|
||||||
"core.login.changepasswordreconnectinstructions": "local_moodlemobileapp",
|
"core.login.changepasswordreconnectinstructions": "local_moodlemobileapp",
|
||||||
"core.login.confirmdeletesite": "local_moodlemobileapp",
|
"core.login.confirmdeletesite": "local_moodlemobileapp",
|
||||||
"core.login.connect": "local_moodlemobileapp",
|
"core.login.connect": "local_moodlemobileapp",
|
||||||
|
@ -1993,6 +1991,7 @@
|
||||||
"core.login.recaptchaincorrect": "local_moodlemobileapp",
|
"core.login.recaptchaincorrect": "local_moodlemobileapp",
|
||||||
"core.login.reconnect": "local_moodlemobileapp",
|
"core.login.reconnect": "local_moodlemobileapp",
|
||||||
"core.login.reconnectssodescription": "local_moodlemobileapp",
|
"core.login.reconnectssodescription": "local_moodlemobileapp",
|
||||||
|
"core.login.reconnecttosite": "local_moodlemobileapp",
|
||||||
"core.login.removeaccount": "local_moodlemobileapp",
|
"core.login.removeaccount": "local_moodlemobileapp",
|
||||||
"core.login.resendemail": "moodle",
|
"core.login.resendemail": "moodle",
|
||||||
"core.login.searchby": "local_moodlemobileapp",
|
"core.login.searchby": "local_moodlemobileapp",
|
||||||
|
@ -2334,6 +2333,10 @@
|
||||||
"core.user": "moodle",
|
"core.user": "moodle",
|
||||||
"core.user.address": "moodle",
|
"core.user.address": "moodle",
|
||||||
"core.user.city": "moodle",
|
"core.user.city": "moodle",
|
||||||
|
"core.user.completeprofile": "local_moodlemobileapp",
|
||||||
|
"core.user.completeprofilenotice": "local_moodlemobileapp",
|
||||||
|
"core.user.completeprofilereconnectinstructions": "local_moodlemobileapp",
|
||||||
|
"core.user.completeyourprofile": "local_moodlemobileapp",
|
||||||
"core.user.contact": "local_moodlemobileapp",
|
"core.user.contact": "local_moodlemobileapp",
|
||||||
"core.user.country": "moodle",
|
"core.user.country": "moodle",
|
||||||
"core.user.description": "moodle",
|
"core.user.description": "moodle",
|
||||||
|
@ -2371,6 +2374,7 @@
|
||||||
"core.vieweditor": "local_moodlemobileapp",
|
"core.vieweditor": "local_moodlemobileapp",
|
||||||
"core.viewembeddedcontent": "local_moodlemobileapp",
|
"core.viewembeddedcontent": "local_moodlemobileapp",
|
||||||
"core.viewprofile": "moodle",
|
"core.viewprofile": "moodle",
|
||||||
|
"core.wanttochangesite": "local_moodlemobileapp",
|
||||||
"core.warningofflinedatadeleted": "local_moodlemobileapp",
|
"core.warningofflinedatadeleted": "local_moodlemobileapp",
|
||||||
"core.warnopeninbrowser": "local_moodlemobileapp",
|
"core.warnopeninbrowser": "local_moodlemobileapp",
|
||||||
"core.week": "moodle",
|
"core.week": "moodle",
|
||||||
|
|
|
@ -52,13 +52,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*
|
|
||||||
* @todo Review all old code to see if something is missing:
|
|
||||||
* - IAB events listening.
|
|
||||||
* - Platform pause/resume subscriptions.
|
|
||||||
* - handleOpenURL and openWindowSafely.
|
|
||||||
* - Back button registering to close modal first.
|
|
||||||
* - Note: HideKeyboardFormAccessoryBar has been moved to config.xml.
|
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -100,24 +93,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
content.classList.toggle('core-footer-shadow', !CoreDom.scrollIsBottom(scrollElement));
|
content.classList.toggle('core-footer-shadow', !CoreDom.scrollIsBottom(scrollElement));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for session expired events.
|
|
||||||
CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data) => {
|
|
||||||
CoreLoginHelper.sessionExpired(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for passwordchange and usernotfullysetup events to open InAppBrowser.
|
|
||||||
CoreEvents.on(CoreEvents.PASSWORD_CHANGE_FORCED, (data) => {
|
|
||||||
CoreLoginHelper.passwordChangeForced(data.siteId!);
|
|
||||||
});
|
|
||||||
CoreEvents.on(CoreEvents.USER_NOT_FULLY_SETUP, (data) => {
|
|
||||||
CoreLoginHelper.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) => {
|
|
||||||
CoreLoginHelper.sitePolicyNotAgreed(data.siteId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check URLs loaded in any InAppBrowser.
|
// Check URLs loaded in any InAppBrowser.
|
||||||
CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => {
|
CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => {
|
||||||
// URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this.
|
// URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this.
|
||||||
|
@ -279,6 +254,9 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
CoreApp.closeApp();
|
CoreApp.closeApp();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @todo: Pause Youtube videos in Android when app is put in background or screen is locked?
|
||||||
|
// See: https://github.com/moodlehq/moodleapp/blob/ionic3/src/app/app.component.ts#L312
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -675,10 +675,12 @@ export class CoreSite {
|
||||||
// Password Change Forced, trigger event. Try to get data from cache, the event will handle the error.
|
// Password Change Forced, trigger event. Try to get data from cache, the event will handle the error.
|
||||||
CoreEvents.trigger(CoreEvents.PASSWORD_CHANGE_FORCED, {}, this.id);
|
CoreEvents.trigger(CoreEvents.PASSWORD_CHANGE_FORCED, {}, this.id);
|
||||||
error.message = Translate.instant('core.forcepasswordchangenotice');
|
error.message = Translate.instant('core.forcepasswordchangenotice');
|
||||||
|
useSilentError = true; // Use a silent error, the change password page already displays the appropiate info.
|
||||||
} else if (error.errorcode === 'usernotfullysetup') {
|
} else if (error.errorcode === 'usernotfullysetup') {
|
||||||
// User not fully setup, trigger event. Try to get data from cache, the event will handle the error.
|
// User not fully setup, trigger event. Try to get data from cache, the event will handle the error.
|
||||||
CoreEvents.trigger(CoreEvents.USER_NOT_FULLY_SETUP, {}, this.id);
|
CoreEvents.trigger(CoreEvents.USER_NOT_FULLY_SETUP, {}, this.id);
|
||||||
error.message = Translate.instant('core.usernotfullysetup');
|
error.message = Translate.instant('core.usernotfullysetup');
|
||||||
|
useSilentError = true; // Use a silent error, the complete profile page already displays the appropiate info.
|
||||||
} else if (error.errorcode === 'sitepolicynotagreed') {
|
} else if (error.errorcode === 'sitepolicynotagreed') {
|
||||||
// Site policy not agreed, trigger event.
|
// Site policy not agreed, trigger event.
|
||||||
CoreEvents.trigger(CoreEvents.SITE_POLICY_NOT_AGREED, {}, this.id);
|
CoreEvents.trigger(CoreEvents.SITE_POLICY_NOT_AGREED, {}, this.id);
|
||||||
|
|
|
@ -4,12 +4,10 @@
|
||||||
"auth_email": "Email-based self-registration",
|
"auth_email": "Email-based self-registration",
|
||||||
"authenticating": "Authenticating",
|
"authenticating": "Authenticating",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"changepassword": "Change password",
|
"changepassword": "Change your password",
|
||||||
"changepasswordbutton": "Open the change password page",
|
"changepasswordbutton": "Change password",
|
||||||
"changepasswordhelp": "If you have problems changing your password, please contact your site administrator. \"Site Administrators\" are the people who manages the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.",
|
"changepasswordhelp": "If you have problems changing your password, please contact your site administrator. \"Site Administrators\" are the people who manages the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.",
|
||||||
"changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.",
|
"changepasswordreconnectinstructions": "If you didn't change your password correctly, you'll be asked to do it again.",
|
||||||
"changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:",
|
|
||||||
"changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).",
|
|
||||||
"confirmdeletesite": "Are you sure you want to remove the account on {{sitename}}?",
|
"confirmdeletesite": "Are you sure you want to remove the account on {{sitename}}?",
|
||||||
"connect": "Connect!",
|
"connect": "Connect!",
|
||||||
"connecttomoodle": "Connect to Moodle",
|
"connecttomoodle": "Connect to Moodle",
|
||||||
|
@ -97,6 +95,7 @@
|
||||||
"recaptchaincorrect": "The security question answer is incorrect.",
|
"recaptchaincorrect": "The security question answer is incorrect.",
|
||||||
"reconnect": "Reconnect",
|
"reconnect": "Reconnect",
|
||||||
"reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.",
|
"reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.",
|
||||||
|
"reconnecttosite": "Reconnect to the site",
|
||||||
"removeaccount": "Remove account",
|
"removeaccount": "Remove account",
|
||||||
"resendemail": "Resend email",
|
"resendemail": "Resend email",
|
||||||
"searchby": "Search by:",
|
"searchby": "Search by:",
|
||||||
|
|
|
@ -16,10 +16,11 @@ import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { AppRoutingModule } from '@/app/app-routing.module';
|
import { AppRoutingModule } from '@/app/app-routing.module';
|
||||||
import { CoreLoginHelperProvider } from './services/login-helper';
|
import { CoreLoginHelper, CoreLoginHelperProvider } from './services/login-helper';
|
||||||
import { CoreRedirectGuard } from '@guards/redirect';
|
import { CoreRedirectGuard } from '@guards/redirect';
|
||||||
import { CoreLoginCronHandler } from './services/handlers/cron';
|
import { CoreLoginCronHandler } from './services/handlers/cron';
|
||||||
import { CoreCronDelegate } from '@services/cron';
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
|
||||||
export const CORE_LOGIN_SERVICES = [
|
export const CORE_LOGIN_SERVICES = [
|
||||||
CoreLoginHelperProvider,
|
CoreLoginHelperProvider,
|
||||||
|
@ -44,6 +45,18 @@ const appRoutes: Routes = [
|
||||||
multi: true,
|
multi: true,
|
||||||
useValue: () => {
|
useValue: () => {
|
||||||
CoreCronDelegate.register(CoreLoginCronHandler.instance);
|
CoreCronDelegate.register(CoreLoginCronHandler.instance);
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data) => {
|
||||||
|
CoreLoginHelper.sessionExpired(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.PASSWORD_CHANGE_FORCED, (data) => {
|
||||||
|
CoreLoginHelper.passwordChangeForced(data.siteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data) => {
|
||||||
|
CoreLoginHelper.sitePolicyNotAgreed(data.siteId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
|
|
||||||
<ion-title>
|
<ion-title>
|
||||||
<h1>{{ 'core.login.changepassword' | translate }}</h1>
|
<h1 *ngIf="!changingPassword">{{ 'core.login.changepassword' | translate }}</h1>
|
||||||
|
<h1 *ngIf="changingPassword">{{ 'core.login.reconnecttosite' | translate }}</h1>
|
||||||
</ion-title>
|
</ion-title>
|
||||||
|
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
|
@ -21,7 +22,6 @@
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'core.login.forcepasswordchangenotice' | translate }}</h2>
|
<h2>{{ 'core.login.forcepasswordchangenotice' | translate }}</h2>
|
||||||
<p class="ion-padding-top">{{ 'core.login.changepasswordinstructions' | translate }}</p>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="openChangePasswordPage()">
|
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="openChangePasswordPage()">
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p>{{ 'core.login.changepasswordlogoutinstructions' | translate }}</p>
|
<p>{{ 'core.wanttochangesite' | translate }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-button class="ion-text-wrap ion-margin" expand="block" fill="outline" (click)="logout()">
|
<ion-button class="ion-text-wrap ion-margin" expand="block" fill="outline" (click)="logout()">
|
||||||
|
|
|
@ -12,13 +12,15 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that shows instructions to change the password.
|
* Page that shows instructions to change the password.
|
||||||
|
@ -27,11 +29,14 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
selector: 'page-core-login-change-password',
|
selector: 'page-core-login-change-password',
|
||||||
templateUrl: 'change-password.html',
|
templateUrl: 'change-password.html',
|
||||||
})
|
})
|
||||||
export class CoreLoginChangePasswordPage {
|
export class CoreLoginChangePasswordPage implements OnDestroy {
|
||||||
|
|
||||||
changingPassword = false;
|
changingPassword = false;
|
||||||
logoutLabel: string;
|
logoutLabel: string;
|
||||||
|
|
||||||
|
protected urlLoadedObserver?: CoreEventObserver;
|
||||||
|
protected browserClosedObserver?: CoreEventObserver;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.logoutLabel = CoreLoginHelper.getLogoutLabel();
|
this.logoutLabel = CoreLoginHelper.getLogoutLabel();
|
||||||
}
|
}
|
||||||
|
@ -57,6 +62,7 @@ export class CoreLoginChangePasswordPage {
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
this.changingPassword = true;
|
this.changingPassword = true;
|
||||||
|
this.detectPasswordChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,4 +81,37 @@ export class CoreLoginChangePasswordPage {
|
||||||
this.changingPassword = false;
|
this.changingPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to detect if the user changed password in browser.
|
||||||
|
*/
|
||||||
|
detectPasswordChanged(): void {
|
||||||
|
if (this.urlLoadedObserver) {
|
||||||
|
// Already listening (shouldn't happen).
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.urlLoadedObserver = CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => {
|
||||||
|
if (event.url.match(/\/login\/change_password\.php.*return=1/)) {
|
||||||
|
// Password should have changed.
|
||||||
|
CoreUtils.closeInAppBrowser();
|
||||||
|
this.login();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.browserClosedObserver = CoreEvents.on(CoreEvents.IAB_EXIT, () => {
|
||||||
|
this.urlLoadedObserver?.off();
|
||||||
|
this.browserClosedObserver?.off();
|
||||||
|
delete this.urlLoadedObserver;
|
||||||
|
delete this.browserClosedObserver;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.urlLoadedObserver?.off();
|
||||||
|
this.browserClosedObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -784,10 +784,12 @@ export class CoreLoginHelperProvider {
|
||||||
/**
|
/**
|
||||||
* Function that should be called when password change is forced. Reserved for core use.
|
* Function that should be called when password change is forced. Reserved for core use.
|
||||||
*
|
*
|
||||||
* @param siteId The site ID.
|
* @param siteId The site ID. Undefined for current site.
|
||||||
*/
|
*/
|
||||||
async passwordChangeForced(siteId: string): Promise<void> {
|
async passwordChangeForced(siteId?: string): Promise<void> {
|
||||||
const currentSite = CoreSites.getCurrentSite();
|
const currentSite = CoreSites.getCurrentSite();
|
||||||
|
siteId = siteId ?? currentSite?.getId();
|
||||||
|
|
||||||
if (!currentSite || siteId !== currentSite.getId()) {
|
if (!currentSite || siteId !== currentSite.getId()) {
|
||||||
return; // Site that triggered the event is not current site.
|
return; // Site that triggered the event is not current site.
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,3 +90,52 @@ Feature: Test basic usage of login in app
|
||||||
| minimumversion | 11.0.0 | tool_mobile |
|
| minimumversion | 11.0.0 | tool_mobile |
|
||||||
When I enter the app
|
When I enter the app
|
||||||
Then I should find "App update required" in the app
|
Then I should find "App update required" in the app
|
||||||
|
|
||||||
|
Scenario: Force password change
|
||||||
|
Given I force a password change for user "student1"
|
||||||
|
When I enter the app
|
||||||
|
And I log in as "student1"
|
||||||
|
Then I should find "Change your password" in the app
|
||||||
|
And I should find "You must change your password to proceed." in the app
|
||||||
|
|
||||||
|
When I press "Change password" in the app
|
||||||
|
Then the app should have opened a browser tab with url "webserver"
|
||||||
|
|
||||||
|
When I close the browser tab opened by the app
|
||||||
|
Then I should find "If you didn't change your password correctly, you'll be asked to do it again." in the app
|
||||||
|
But I should not find "Change your password" in the app
|
||||||
|
|
||||||
|
When I press "Reconnect" in the app
|
||||||
|
Then I should find "Change your password" in the app
|
||||||
|
But I should not find "Reconnect" in the app
|
||||||
|
|
||||||
|
When I press "Switch account" in the app
|
||||||
|
Then I should find "Accounts" in the app
|
||||||
|
And I should find "david student" in the app
|
||||||
|
|
||||||
|
When I press "david student" in the app
|
||||||
|
Then I should find "Change your password" in the app
|
||||||
|
But I should not find "Reconnect" in the app
|
||||||
|
|
||||||
|
When I press "Change password" in the app
|
||||||
|
Then the app should have opened a browser tab with url "webserver"
|
||||||
|
|
||||||
|
When I switch to the browser tab opened by the app
|
||||||
|
And I set the field "username" to "student1"
|
||||||
|
And I set the field "password" to "student1"
|
||||||
|
And I click on "Log in" "button"
|
||||||
|
Then I should see "You must change your password to proceed"
|
||||||
|
|
||||||
|
When I set the field "Current password" to "student1"
|
||||||
|
And I set the field "New password" to "NewPassword1*"
|
||||||
|
And I set the field "New password (again)" to "NewPassword1*"
|
||||||
|
And I click on "Sign out everywhere" "checkbox"
|
||||||
|
And I click on "Save changes" "button"
|
||||||
|
Then I should see "Password has been changed"
|
||||||
|
|
||||||
|
When I close the browser tab opened by the app
|
||||||
|
Then I should find "If you didn't change your password correctly, you'll be asked to do it again." in the app
|
||||||
|
But I should not find "Change your password" in the app
|
||||||
|
|
||||||
|
When I press "Reconnect" in the app
|
||||||
|
Then I should find "Acceptance test site" in the app
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"useraccount": "User account",
|
"useraccount": "User account",
|
||||||
"city": "City/town",
|
"city": "City/town",
|
||||||
|
"completeprofile": "Complete profile",
|
||||||
|
"completeprofilenotice": "Before you continue, please fill in the required fields in your user profile.",
|
||||||
|
"completeprofilereconnectinstructions": "If you didn't complete your profile correctly, you'll be asked to do it again.",
|
||||||
|
"completeyourprofile": "Complete your profile",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
|
||||||
|
<ion-title>
|
||||||
|
<h1 *ngIf="!editingProfile">{{ 'core.user.completeyourprofile' | translate }}</h1>
|
||||||
|
<h1 *ngIf="editingProfile">{{ 'core.login.reconnecttosite' | translate }}</h1>
|
||||||
|
</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button fill="clear" (click)="showHelp()" [attr.aria-label]="'core.help' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="far-question-circle" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-list class="list-item-limited-width">
|
||||||
|
<ng-container *ngIf="!editingProfile">
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<p>{{ 'core.user.completeprofilenotice' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="openCompleteProfilePage()">
|
||||||
|
{{ 'core.user.completeprofile' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="editingProfile">
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<p>{{ 'core.user.completeprofilereconnectinstructions' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="login()">
|
||||||
|
{{ 'core.login.reconnect' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<p>{{ 'core.wanttochangesite' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-button class="ion-text-wrap ion-margin" expand="block" fill="outline" (click)="logout()">
|
||||||
|
{{ logoutLabel | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,118 @@
|
||||||
|
// (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, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that shows instructions to complete the profile.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-user-complete-profile',
|
||||||
|
templateUrl: 'complete-profile.html',
|
||||||
|
})
|
||||||
|
export class CoreUserCompleteProfilePage implements OnDestroy {
|
||||||
|
|
||||||
|
editingProfile = false;
|
||||||
|
logoutLabel: string;
|
||||||
|
|
||||||
|
protected urlLoadedObserver?: CoreEventObserver;
|
||||||
|
protected browserClosedObserver?: CoreEventObserver;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logoutLabel = CoreLoginHelper.getLogoutLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a help modal.
|
||||||
|
*/
|
||||||
|
showHelp(): void {
|
||||||
|
// @todo MOBILE-4059: Change this message.
|
||||||
|
CoreDomUtils.showAlert(
|
||||||
|
Translate.instant('core.help'),
|
||||||
|
Translate.instant('core.login.changepasswordhelp'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the edit profile page in a browser.
|
||||||
|
*/
|
||||||
|
openCompleteProfilePage(): void {
|
||||||
|
CoreLoginHelper.openInAppForEdit(
|
||||||
|
CoreSites.getCurrentSiteId(),
|
||||||
|
'/user/edit.php',
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.editingProfile = true;
|
||||||
|
this.detectProileEdited();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login the user.
|
||||||
|
*/
|
||||||
|
login(): void {
|
||||||
|
CoreNavigator.navigateToSiteHome();
|
||||||
|
this.editingProfile = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout the user.
|
||||||
|
*/
|
||||||
|
logout(): void {
|
||||||
|
CoreSites.logout();
|
||||||
|
this.editingProfile = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to detect if the user edited the profile in browser.
|
||||||
|
*/
|
||||||
|
detectProileEdited(): void {
|
||||||
|
if (this.urlLoadedObserver) {
|
||||||
|
// Already listening (shouldn't happen).
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.urlLoadedObserver = CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => {
|
||||||
|
if (event.url.match(/\/user\/preferences.php/)) {
|
||||||
|
// Profile should be complete now.
|
||||||
|
CoreUtils.closeInAppBrowser();
|
||||||
|
this.login();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.browserClosedObserver = CoreEvents.on(CoreEvents.IAB_EXIT, () => {
|
||||||
|
this.urlLoadedObserver?.off();
|
||||||
|
this.browserClosedObserver?.off();
|
||||||
|
delete this.urlLoadedObserver;
|
||||||
|
delete this.browserClosedObserver;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.urlLoadedObserver?.off();
|
||||||
|
this.browserClosedObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreUserRole } from './user';
|
import { CoreUserRole } from './user';
|
||||||
|
@ -60,6 +62,27 @@ export class CoreUserHelperProvider {
|
||||||
}).join(separator + ' ');
|
}).join(separator + ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a page with instructions on how to complete profile.
|
||||||
|
*
|
||||||
|
* @param siteId The site ID. Undefined for current site.
|
||||||
|
*/
|
||||||
|
async openCompleteProfile(siteId?: string): Promise<void> {
|
||||||
|
const currentSite = CoreSites.getCurrentSite();
|
||||||
|
siteId = siteId ?? currentSite?.getId();
|
||||||
|
|
||||||
|
if (!currentSite || siteId !== currentSite.getId()) {
|
||||||
|
return; // Site that triggered the event is not current site.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current page is already complete profile, stop.
|
||||||
|
if (CoreNavigator.isCurrent('/user/completeprofile')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreNavigator.navigate('/user/completeprofile', { params: { siteId }, reset: true });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreUserHelper = makeSingleton(CoreUserHelperProvider);
|
export const CoreUserHelper = makeSingleton(CoreUserHelperProvider);
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
@core @core_user @app @javascript
|
||||||
|
Feature: Test basic usage of user features
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname |
|
||||||
|
| student1 | Student | Student |
|
||||||
|
|
||||||
|
Scenario: Complete missing fields
|
||||||
|
Given the following "custom profile fields" exist:
|
||||||
|
| datatype | shortname | name | required |
|
||||||
|
| text | food | Favourite food | 1 |
|
||||||
|
When I enter the app
|
||||||
|
And I log in as "student1"
|
||||||
|
Then I should find "Complete your profile" in the app
|
||||||
|
And I should find "Before you continue, please fill in the required fields in your user profile." in the app
|
||||||
|
|
||||||
|
When I press "Complete profile" in the app
|
||||||
|
Then the app should have opened a browser tab with url "webserver"
|
||||||
|
|
||||||
|
When I close the browser tab opened by the app
|
||||||
|
Then I should find "If you didn't complete your profile correctly, you'll be asked to do it again." in the app
|
||||||
|
But I should not find "Complete your profile" in the app
|
||||||
|
|
||||||
|
When I press "Reconnect" in the app
|
||||||
|
Then I should find "Complete your profile" in the app
|
||||||
|
But I should not find "Reconnect" in the app
|
||||||
|
|
||||||
|
When I press "Switch account" in the app
|
||||||
|
Then I should find "Accounts" in the app
|
||||||
|
And I should find "Student Student" in the app
|
||||||
|
|
||||||
|
When I press "Student Student" in the app
|
||||||
|
Then I should find "Complete your profile" in the app
|
||||||
|
But I should not find "Reconnect" in the app
|
||||||
|
|
||||||
|
When I press "Complete profile" in the app
|
||||||
|
Then the app should have opened a browser tab with url "webserver"
|
||||||
|
|
||||||
|
When I switch to the browser tab opened by the app
|
||||||
|
And I set the field "username" to "student1"
|
||||||
|
And I set the field "password" to "student1"
|
||||||
|
And I click on "Log in" "button"
|
||||||
|
And I set the field "Favourite food" to "Pasta"
|
||||||
|
And I click on "Update profile" "button"
|
||||||
|
Then I should see "Changes saved"
|
||||||
|
|
||||||
|
When I close the browser tab opened by the app
|
||||||
|
Then I should find "If you didn't complete your profile correctly, you'll be asked to do it again." in the app
|
||||||
|
But I should not find "Complete your profile" in the app
|
||||||
|
|
||||||
|
When I press "Reconnect" in the app
|
||||||
|
Then I should find "Acceptance test site" in the app
|
|
@ -0,0 +1,37 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CoreUserCompleteProfilePage } from './pages/complete-profile/complete-profile';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'completeprofile',
|
||||||
|
component: CoreUserCompleteProfilePage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreUserCompleteProfilePage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreUserAppLazyModule {}
|
|
@ -32,13 +32,14 @@ import { CoreCourseOptionsDelegate } from '@features/course/services/course-opti
|
||||||
import { CoreUserCourseOptionHandler } from './services/handlers/course-option';
|
import { CoreUserCourseOptionHandler } from './services/handlers/course-option';
|
||||||
import { CoreUserProfileFieldDelegateService } from './services/user-profile-field-delegate';
|
import { CoreUserProfileFieldDelegateService } from './services/user-profile-field-delegate';
|
||||||
import { CoreUserProvider } from './services/user';
|
import { CoreUserProvider } from './services/user';
|
||||||
import { CoreUserHelperProvider } from './services/user-helper';
|
import { CoreUserHelper, CoreUserHelperProvider } from './services/user-helper';
|
||||||
import { CoreUserOfflineProvider } from './services/user-offline';
|
import { CoreUserOfflineProvider } from './services/user-offline';
|
||||||
import { CoreUserSyncProvider } from './services/user-sync';
|
import { CoreUserSyncProvider } from './services/user-sync';
|
||||||
import { conditionalRoutes } from '@/app/app-routing.module';
|
import { AppRoutingModule, conditionalRoutes } from '@/app/app-routing.module';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { COURSE_PAGE_NAME } from '@features/course/course.module';
|
import { COURSE_PAGE_NAME } from '@features/course/course.module';
|
||||||
import { COURSE_INDEX_PATH } from '@features/course/course-lazy.module';
|
import { COURSE_INDEX_PATH } from '@features/course/course-lazy.module';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
|
||||||
export const CORE_USER_SERVICES: Type<unknown>[] = [
|
export const CORE_USER_SERVICES: Type<unknown>[] = [
|
||||||
CoreUserDelegateService,
|
CoreUserDelegateService,
|
||||||
|
@ -51,6 +52,13 @@ export const CORE_USER_SERVICES: Type<unknown>[] = [
|
||||||
|
|
||||||
export const PARTICIPANTS_PAGE_NAME = 'participants';
|
export const PARTICIPANTS_PAGE_NAME = 'participants';
|
||||||
|
|
||||||
|
const appRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
loadChildren: () => import('@features/user/user-app-lazy.module').then(m => m.CoreUserAppLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'user',
|
path: 'user',
|
||||||
|
@ -76,6 +84,7 @@ const courseIndexRoutes: Routes = [
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
AppRoutingModule.forChild(appRoutes),
|
||||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||||
CoreUserComponentsModule,
|
CoreUserComponentsModule,
|
||||||
|
@ -98,6 +107,10 @@ const courseIndexRoutes: Routes = [
|
||||||
CoreCronDelegate.register(CoreUserSyncCronHandler.instance);
|
CoreCronDelegate.register(CoreUserSyncCronHandler.instance);
|
||||||
CoreTagAreaDelegate.registerHandler(CoreUserTagAreaHandler.instance);
|
CoreTagAreaDelegate.registerHandler(CoreUserTagAreaHandler.instance);
|
||||||
CoreCourseOptionsDelegate.registerHandler(CoreUserCourseOptionHandler.instance);
|
CoreCourseOptionsDelegate.registerHandler(CoreUserCourseOptionHandler.instance);
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.USER_NOT_FULLY_SETUP, (data) => {
|
||||||
|
CoreUserHelper.openCompleteProfile(data.siteId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -340,6 +340,7 @@
|
||||||
"vieweditor": "View editor",
|
"vieweditor": "View editor",
|
||||||
"viewembeddedcontent": "View embedded content",
|
"viewembeddedcontent": "View embedded content",
|
||||||
"viewprofile": "View profile",
|
"viewprofile": "View profile",
|
||||||
|
"wanttochangesite": "Want to change sites or log out?",
|
||||||
"warningofflinedatadeleted": "Offline data from {{component}} '{{name}}' has been deleted. {{error}}",
|
"warningofflinedatadeleted": "Offline data from {{component}} '{{name}}' has been deleted. {{error}}",
|
||||||
"warnopeninbrowser": "<p>You are about to leave the app to open the following URL in your device's browser. Do you want to continue?</p>\n<p><b>{{url}}</b></p>",
|
"warnopeninbrowser": "<p>You are about to leave the app to open the following URL in your device's browser. Do you want to continue?</p>\n<p><b>{{url}}</b></p>",
|
||||||
"week": "Week",
|
"week": "Week",
|
||||||
|
|
Loading…
Reference in New Issue