MOBILE-4059 mainmenu: Add link to contact support

main
Noel De Martin 2022-09-21 11:12:53 +02:00
parent 95e0640eb2
commit 4eb01a063c
10 changed files with 219 additions and 5 deletions

View File

@ -2363,6 +2363,7 @@
"core.user.roles": "moodle",
"core.user.sendemail": "local_moodlemobileapp",
"core.user.student": "moodle/defaultcoursestudent",
"core.user.support": "local_moodlemobileapp",
"core.user.teacher": "moodle/noneditingteacher",
"core.user.useraccount": "moodle",
"core.user.userwithid": "local_moodlemobileapp",

View File

@ -60,6 +60,7 @@ import {
import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs';
import { finalize, map, mergeMap } from 'rxjs/operators';
import { firstValueFrom } from '../utils/rxjs';
import { CoreUserSupport } from '@features/user/services/support';
/**
* QR Code type enumeration.
@ -262,6 +263,19 @@ export class CoreSite {
return this.db;
}
/**
* Get url to contact site support.
*
* @returns Site support page url.
*/
getSupportPageUrl(): string | null {
if (!this.config || !this.canContactSupport()) {
return null;
}
return CoreUserSupport.getSupportPageUrl(this.config, this.siteUrl);
}
/**
* Get site user's ID.
*
@ -421,6 +435,19 @@ export class CoreSite {
return !!(info && (info.usercanmanageownfiles === undefined || info.usercanmanageownfiles));
}
/**
* Check whether this site has a support url available.
*
* @returns Whether this site has a support url.
*/
canContactSupport(): boolean {
if (this.isFeatureDisabled('NoDelegate_CoreUserSupport')) {
return false;
}
return !!this.config && CoreUserSupport.canContactSupport(this.config);
}
/**
* Can the user download files?
*
@ -2777,6 +2804,7 @@ export type CoreSitePublicConfigResponse = {
agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled.
supportname?: string; // Site support contact name (only if age verification is enabled).
supportemail?: string; // Site support contact email (only if age verification is enabled).
supportpage?: string; // Site support contact url.
autolang?: number; // Whether to detect default language from browser setting.
lang?: string; // Default language for the site.
langmenu?: number; // Whether the language menu should be displayed.

View File

@ -66,6 +66,14 @@
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
</ion-label>
</ion-item>
<ion-item *ngIf="displayContactSupport" button (click)="contactSupport($event)"
[attr.aria-label]="'core.user.support' | translate" detail="true" detailIcon="open-outline" class="core-user-menu-support">
<ion-icon name="fas-envelope" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.user.support' | translate }}</p>
</ion-label>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -18,6 +18,7 @@ import { CoreSite, CoreSiteInfo } from '@classes/site';
import { CoreFilter } from '@features/filter/services/filter';
import { CoreLoginSitesComponent } from '@features/login/components/sites/sites';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreUserSupport } from '@features/user/services/support';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import {
CoreUserProfileHandlerData,
@ -51,6 +52,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
handlersLoaded = false;
user?: CoreUserProfile;
displaySwitchAccount = true;
displayContactSupport = false;
removeAccountOnLogout = false;
protected subscription!: Subscription;
@ -65,6 +67,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
this.siteName = currentSite.getSiteName();
this.siteUrl = currentSite.getURL();
this.displaySwitchAccount = !currentSite.isFeatureDisabled('NoDelegate_SwitchAccount');
this.displayContactSupport = currentSite.canContactSupport();
this.removeAccountOnLogout = !!CoreConstants.CONFIG.removeaccountonlogout;
this.loadSiteLogo(currentSite);
@ -173,6 +176,16 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
handler.action(event, this.user, CoreUserDelegateContext.USER_MENU);
}
/**
* Contact site support.
*
* @param event Click event.
*/
async contactSupport(event: Event): Promise<void> {
await this.close(event);
await CoreUserSupport.contact();
}
/**
* Logout the user.
*

View File

@ -28,6 +28,7 @@
"roles": "Roles",
"sendemail": "Email",
"student": "Student",
"support": "Support",
"teacher": "Non-editing teacher",
"userwithid": "User with ID {{id}}",
"webpage": "Web page"

View File

@ -0,0 +1,112 @@
// (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 { CoreError } from '@classes/errors/error';
import { CoreSiteConfig, CoreSitePublicConfigResponse } from '@classes/site';
import { InAppBrowserObject } from '@ionic-native/in-app-browser';
import { CorePlatform } from '@services/platform';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreSubscriptions } from '@singletons/subscriptions';
/**
* Handle site support.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserSupportService {
/**
* Contact site support.
*
* @param options Options to configure the interaction with support.
*/
async contact(options: CoreUserSupportContactOptions = {}): Promise<void> {
const supportPageUrl = options.supportPageUrl ?? CoreSites.getRequiredCurrentSite().getSupportPageUrl();
if (!supportPageUrl) {
throw new CoreError('Could not get support url');
}
const autoLoginUrl = await CoreSites.getCurrentSite()?.getAutoLoginUrl(supportPageUrl, false);
const browser = CoreUtils.openInApp(autoLoginUrl ?? supportPageUrl);
if (supportPageUrl.endsWith('/user/contactsitesupport.php')) {
this.populateSupportForm(browser, options.subject, options.message);
}
await CoreEvents.waitUntil(CoreEvents.IAB_EXIT);
}
/**
* Get support page url from site config.
*
* @param config Site config.
* @returns Support page url.
*/
getSupportPageUrl(config: CoreSitePublicConfigResponse): string;
getSupportPageUrl(config: CoreSiteConfig, siteUrl: string): string;
getSupportPageUrl(config: CoreSiteConfig | CoreSitePublicConfigResponse, siteUrl?: string): string {
return config.supportpage?.trim()
|| `${config.httpswwwroot ?? config.wwwroot ?? siteUrl}/user/contactsitesupport.php`;
}
/**
* Check whether a site config allows contacting support.
*
* @param config Site config.
* @returns Whether site support can be contacted.
*/
canContactSupport(config: CoreSiteConfig | CoreSitePublicConfigResponse): boolean {
return 'supportpage' in config;
}
/**
* Inject error details into contact support form.
*
* @param browser In App browser containing the support form.
* @param subject Title to fill into the form.
* @param message Details to fill into the form.
*/
protected populateSupportForm(browser: InAppBrowserObject, subject?: string | null, message?: string | null): void {
if (!CorePlatform.isMobile()) {
return;
}
const unsubscribe = CoreSubscriptions.once(browser.on('loadstop'), () => {
browser.executeScript({
code: `
document.querySelector('#id_subject').value = ${JSON.stringify(subject ?? '')};
document.querySelector('#id_message').value = ${JSON.stringify(message ?? '')};
`,
});
});
CoreEvents.once(CoreEvents.IAB_EXIT, () => unsubscribe());
}
}
export const CoreUserSupport = makeSingleton(CoreUserSupportService);
/**
* Options to configure interaction with support.
*/
export interface CoreUserSupportContactOptions {
supportPageUrl?: string | null;
subject?: string | null;
message?: string | null;
}

View File

@ -0,0 +1,13 @@
@core @core_user @app @javascript @lms_upto3.11
Feature: Site support
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student1 | Student | Student |
Scenario: Cannot contact support
Given I entered the app as "student1"
When I press the user menu button in the app
Then I should find "Blog entries" in the app
But I should not find "Support" in the app

View File

@ -0,0 +1,33 @@
@core @core_user @app @javascript @lms_from4.0
Feature: Site support
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student1 | Student | Student |
Scenario: Uses default support page
Given I entered the app as "student1"
When I press the user menu button in the app
Then I should find "Support" in the app
When I press "Support" in the app
Then the app should have opened a browser tab with url ".*\/user\/contactsitesupport\.php"
Scenario: Uses custom support page
Given the following config values are set as admin:
| supportpage | https://campus.example.edu/support |
And I entered the app as "student1"
When I press the user menu button in the app
Then I should find "Support" in the app
When I press "Support" in the app
Then the app should have opened a browser tab with url "https:\/\/campus\.example\.edu\/support"
Scenario: Cannot contact support
Given the following config values are set as admin:
| disabledfeatures | NoDelegate_CoreUserSupport | tool_mobile |
And I entered the app as "student1"
When I press the user menu button in the app
Then I should find "Blog entries" in the app
But I should not find "Support" in the app

View File

@ -1044,11 +1044,7 @@ export class CoreUtilsProvider {
* @param options Override default options passed to InAppBrowser.
* @return The opened window.
*/
openInApp(url: string, options?: InAppBrowserOptions): InAppBrowserObject | undefined {
if (!url) {
return;
}
openInApp(url: string, options?: InAppBrowserOptions): InAppBrowserObject {
options = options || {};
options.usewkwebview = 'yes'; // Force WKWebView in iOS.
options.enableViewPortScale = options.enableViewPortScale ?? 'yes'; // Enable zoom on iOS by default.

View File

@ -282,6 +282,15 @@ export class CoreEvents {
}
}
/**
* Wait until an event has been emitted.
*
* @param eventName Event name.
*/
static waitUntil(eventName: string): Promise<void> {
return new Promise(resolve => this.once(eventName, () => resolve()));
}
}
/**