From c91e1192ae2c993db52dcfc2ec7914f1d9244dea Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 20 Jul 2022 14:44:07 +0200 Subject: [PATCH] MOBILE-4034 user: Improve UX when missing required fields --- scripts/langindex.json | 8 +- src/app/app.component.ts | 3 +- src/core/classes/site.ts | 2 + src/core/features/login/lang.json | 9 +- .../change-password/change-password.html | 6 +- src/core/features/user/lang.json | 4 + .../complete-profile/complete-profile.html | 50 ++++++++ .../complete-profile/complete-profile.ts | 118 ++++++++++++++++++ .../features/user/services/user-helper.ts | 21 ++++ .../features/user/user-app-lazy.module.ts | 37 ++++++ src/core/features/user/user.module.ts | 10 +- src/core/lang.json | 1 + 12 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 src/core/features/user/pages/complete-profile/complete-profile.html create mode 100644 src/core/features/user/pages/complete-profile/complete-profile.ts create mode 100644 src/core/features/user/user-app-lazy.module.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5a015724d..ae08079ec 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ec1d66cc4..5b75b9b9a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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. diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 5fcd79060..7580b54f3 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -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); diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index 6eb0b227f..acbdb3cff 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -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:", diff --git a/src/core/features/login/pages/change-password/change-password.html b/src/core/features/login/pages/change-password/change-password.html index 7b808cbd7..755a642f9 100644 --- a/src/core/features/login/pages/change-password/change-password.html +++ b/src/core/features/login/pages/change-password/change-password.html @@ -5,7 +5,8 @@ -

{{ 'core.login.changepassword' | translate }}

+

{{ 'core.login.changepassword' | translate }}

+

{{ 'core.login.reconnecttosite' | translate }}

@@ -21,7 +22,6 @@

{{ 'core.login.forcepasswordchangenotice' | translate }}

-

{{ 'core.login.changepasswordinstructions' | translate }}

@@ -40,7 +40,7 @@ -

{{ 'core.login.changepasswordlogoutinstructions' | translate }}

+

{{ 'core.wanttochangesite' | translate }}

diff --git a/src/core/features/user/lang.json b/src/core/features/user/lang.json index 978102d21..147b26797 100644 --- a/src/core/features/user/lang.json +++ b/src/core/features/user/lang.json @@ -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", diff --git a/src/core/features/user/pages/complete-profile/complete-profile.html b/src/core/features/user/pages/complete-profile/complete-profile.html new file mode 100644 index 000000000..5c8910bd6 --- /dev/null +++ b/src/core/features/user/pages/complete-profile/complete-profile.html @@ -0,0 +1,50 @@ + + + + + + + +

{{ 'core.user.completeyourprofile' | translate }}

+

{{ 'core.login.reconnecttosite' | translate }}

+
+ + + + + + +
+
+ + + + + +

{{ 'core.user.completeprofilenotice' | translate }}

+
+
+ + {{ 'core.user.completeprofile' | translate }} + +
+ + + +

{{ 'core.user.completeprofilereconnectinstructions' | translate }}

+
+
+ + {{ 'core.login.reconnect' | translate }} + +
+ + +

{{ 'core.wanttochangesite' | translate }}

+
+
+ + {{ logoutLabel | translate }} + +
+
diff --git a/src/core/features/user/pages/complete-profile/complete-profile.ts b/src/core/features/user/pages/complete-profile/complete-profile.ts new file mode 100644 index 000000000..129be105d --- /dev/null +++ b/src/core/features/user/pages/complete-profile/complete-profile.ts @@ -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(); + } + +} diff --git a/src/core/features/user/services/user-helper.ts b/src/core/features/user/services/user-helper.ts index ba08890b2..5e3779eb5 100644 --- a/src/core/features/user/services/user-helper.ts +++ b/src/core/features/user/services/user-helper.ts @@ -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 { + 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); diff --git a/src/core/features/user/user-app-lazy.module.ts b/src/core/features/user/user-app-lazy.module.ts new file mode 100644 index 000000000..d036817e8 --- /dev/null +++ b/src/core/features/user/user-app-lazy.module.ts @@ -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 {} diff --git a/src/core/features/user/user.module.ts b/src/core/features/user/user.module.ts index a8e2398a5..c5816dabe 100644 --- a/src/core/features/user/user.module.ts +++ b/src/core/features/user/user.module.ts @@ -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[] = [ 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, diff --git a/src/core/lang.json b/src/core/lang.json index c83d0aed7..e61537e0e 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -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": "

You are about to leave the app to open the following URL in your device's browser. Do you want to continue?

\n

{{url}}

", "week": "Week",