MOBILE-4034 user: Improve UX when missing required fields

main
Dani Palou 2022-07-20 14:44:07 +02:00
parent 94444fbc96
commit c91e1192ae
12 changed files with 257 additions and 12 deletions

View File

@ -1903,8 +1903,6 @@
"core.login.changepassword": "moodle",
"core.login.changepasswordbutton": "local_moodlemobileapp",
"core.login.changepasswordhelp": "local_moodlemobileapp",
"core.login.changepasswordinstructions": "local_moodlemobileapp",
"core.login.changepasswordlogoutinstructions": "local_moodlemobileapp",
"core.login.changepasswordreconnectinstructions": "local_moodlemobileapp",
"core.login.confirmdeletesite": "local_moodlemobileapp",
"core.login.connect": "local_moodlemobileapp",
@ -1993,6 +1991,7 @@
"core.login.recaptchaincorrect": "local_moodlemobileapp",
"core.login.reconnect": "local_moodlemobileapp",
"core.login.reconnectssodescription": "local_moodlemobileapp",
"core.login.reconnecttosite": "local_moodlemobileapp",
"core.login.removeaccount": "local_moodlemobileapp",
"core.login.resendemail": "moodle",
"core.login.searchby": "local_moodlemobileapp",
@ -2334,6 +2333,10 @@
"core.user": "moodle",
"core.user.address": "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.country": "moodle",
"core.user.description": "moodle",
@ -2371,6 +2374,7 @@
"core.vieweditor": "local_moodlemobileapp",
"core.viewembeddedcontent": "local_moodlemobileapp",
"core.viewprofile": "moodle",
"core.wanttochangesite": "local_moodlemobileapp",
"core.warningofflinedatadeleted": "local_moodlemobileapp",
"core.warnopeninbrowser": "local_moodlemobileapp",
"core.week": "moodle",

View File

@ -34,6 +34,7 @@ import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreDom } from '@singletons/dom';
import { CorePlatform } from '@services/platform';
import { CoreUserHelper } from '@features/user/services/user-helper';
const MOODLE_VERSION_PREFIX = 'version-';
const MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
@ -110,7 +111,7 @@ export class AppComponent implements OnInit, AfterViewInit {
CoreLoginHelper.passwordChangeForced(data.siteId!);
});
CoreEvents.on(CoreEvents.USER_NOT_FULLY_SETUP, (data) => {
CoreLoginHelper.openInAppForEdit(data.siteId!, '/user/edit.php', 'core.usernotfullysetup');
CoreUserHelper.openCompleteProfile(data.siteId!);
});
// Listen for sitepolicynotagreed event to accept the site policy.

View File

@ -675,10 +675,12 @@ export class CoreSite {
// 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);
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') {
// 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);
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') {
// Site policy not agreed, trigger event.
CoreEvents.trigger(CoreEvents.SITE_POLICY_NOT_AGREED, {}, this.id);

View File

@ -4,12 +4,10 @@
"auth_email": "Email-based self-registration",
"authenticating": "Authenticating",
"cancel": "Cancel",
"changepassword": "Change password",
"changepasswordbutton": "Open the change password page",
"changepassword": "Change your password",
"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.",
"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.",
"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).",
"changepasswordreconnectinstructions": "If you didn't change your password correctly, you'll be asked to do it again.",
"confirmdeletesite": "Are you sure you want to remove the account on {{sitename}}?",
"connect": "Connect!",
"connecttomoodle": "Connect to Moodle",
@ -97,6 +95,7 @@
"recaptchaincorrect": "The security question answer is incorrect.",
"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.",
"reconnecttosite": "Reconnect to the site",
"removeaccount": "Remove account",
"resendemail": "Resend email",
"searchby": "Search by:",

View File

@ -5,7 +5,8 @@
</ion-buttons>
<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-buttons slot="end">
@ -21,7 +22,6 @@
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.login.forcepasswordchangenotice' | translate }}</h2>
<p class="ion-padding-top">{{ 'core.login.changepasswordinstructions' | translate }}</p>
</ion-label>
</ion-item>
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="openChangePasswordPage()">
@ -40,7 +40,7 @@
</ng-container>
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'core.login.changepasswordlogoutinstructions' | translate }}</p>
<p>{{ 'core.wanttochangesite' | translate }}</p>
</ion-label>
</ion-item>
<ion-button class="ion-text-wrap ion-margin" expand="block" fill="outline" (click)="logout()">

View File

@ -2,6 +2,10 @@
"address": "Address",
"useraccount": "User account",
"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",
"country": "Country",
"description": "Description",

View File

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

View File

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

View File

@ -13,6 +13,8 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { makeSingleton, Translate } from '@singletons';
import { CoreUserRole } from './user';
@ -60,6 +62,25 @@ export class CoreUserHelperProvider {
}).join(separator + ' ');
}
/**
* Open a page with instructions on how to complete profile.
*
* @param siteId The site ID.
*/
async openCompleteProfile(siteId: string): Promise<void> {
const currentSite = CoreSites.getCurrentSite();
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);

View File

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

View File

@ -35,7 +35,7 @@ import { CoreUserProvider } from './services/user';
import { CoreUserHelperProvider } from './services/user-helper';
import { CoreUserOfflineProvider } from './services/user-offline';
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 { COURSE_PAGE_NAME } from '@features/course/course.module';
import { COURSE_INDEX_PATH } from '@features/course/course-lazy.module';
@ -51,6 +51,13 @@ export const CORE_USER_SERVICES: Type<unknown>[] = [
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 = [
{
path: 'user',
@ -76,6 +83,7 @@ const courseIndexRoutes: Routes = [
@NgModule({
imports: [
AppRoutingModule.forChild(appRoutes),
CoreMainMenuTabRoutingModule.forChild(routes),
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
CoreUserComponentsModule,

View File

@ -340,6 +340,7 @@
"vieweditor": "View editor",
"viewembeddedcontent": "View embedded content",
"viewprofile": "View profile",
"wanttochangesite": "Want to change sites or log out?",
"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>",
"week": "Week",