Merge pull request #2700 from dpalou/MOBILE-3712

Mobile 3712
main
Dani Palou 2021-03-10 08:51:34 +01:00 committed by GitHub
commit bdd787cd07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1082 additions and 107 deletions

View File

@ -12,7 +12,7 @@
// 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 { Observable } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { AppComponent } from '@/app/app.component'; import { AppComponent } from '@/app/app.component';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
@ -31,7 +31,7 @@ describe('AppComponent', () => {
beforeEach(() => { beforeEach(() => {
mockSingleton(CoreApp, { setStatusBarColor: jest.fn() }); mockSingleton(CoreApp, { setStatusBarColor: jest.fn() });
mockSingleton(Network, { onChange: () => new Observable() }); mockSingleton(Network, { onChange: () => new Observable() });
mockSingleton(Platform, { ready: () => Promise.resolve() }); mockSingleton(Platform, { ready: () => Promise.resolve(), resume: new Subject<void>() });
mockSingleton(NgZone, { run: jest.fn() }); mockSingleton(NgZone, { run: jest.fn() });
navigator = mockSingleton(CoreNavigator, ['navigate']); navigator = mockSingleton(CoreNavigator, ['navigate']);

View File

@ -29,6 +29,10 @@ import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreSubscriptions } from '@singletons/subscriptions';
import { CoreWindow } from '@singletons/window';
import { CoreCustomURLSchemes } from '@services/urlschemes';
import { CoreUtils } from '@services/utils/utils';
import { CoreUrlUtils } from '@services/utils/url';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -39,6 +43,9 @@ export class AppComponent implements OnInit, AfterViewInit {
@ViewChild(IonRouterOutlet) outlet?: IonRouterOutlet; @ViewChild(IonRouterOutlet) outlet?: IonRouterOutlet;
protected lastUrls: Record<string, number> = {};
protected lastInAppUrl?: string;
/** /**
* Component being initialized. * Component being initialized.
* *
@ -51,6 +58,9 @@ export class AppComponent implements OnInit, AfterViewInit {
* - Note: HideKeyboardFormAccessoryBar has been moved to config.xml. * - Note: HideKeyboardFormAccessoryBar has been moved to config.xml.
*/ */
ngOnInit(): void { ngOnInit(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = <any> window;
CoreEvents.on(CoreEvents.LOGOUT, () => { CoreEvents.on(CoreEvents.LOGOUT, () => {
// Go to sites page when user is logged out. // Go to sites page when user is logged out.
CoreNavigator.navigate('/login/sites', { reset: true }); CoreNavigator.navigate('/login/sites', { reset: true });
@ -80,6 +90,83 @@ export class AppComponent implements OnInit, AfterViewInit {
CoreLoginHelper.sitePolicyNotAgreed(data.siteId); CoreLoginHelper.sitePolicyNotAgreed(data.siteId);
}); });
// Check URLs loaded in any InAppBrowser.
CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => {
// URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this.
const url = event.url.replace(/^https?:\/\//, '');
if (CoreCustomURLSchemes.isCustomURL(url)) {
// Close the browser if it's a valid SSO URL.
CoreCustomURLSchemes.handleCustomURL(url).catch((error) => {
CoreCustomURLSchemes.treatHandleCustomURLError(error);
});
CoreUtils.closeInAppBrowser();
} else if (CoreApp.instance.isAndroid()) {
// Check if the URL has a custom URL scheme. In Android they need to be opened manually.
const urlScheme = CoreUrlUtils.getUrlProtocol(url);
if (urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile') {
// Open in browser should launch the right app if found and do nothing if not found.
CoreUtils.openInBrowser(url);
// At this point the InAppBrowser is showing a "Webpage not available" error message.
// Try to navigate to last loaded URL so this error message isn't found.
if (this.lastInAppUrl) {
CoreUtils.openInApp(this.lastInAppUrl);
} else {
// No last URL loaded, close the InAppBrowser.
CoreUtils.closeInAppBrowser();
}
} else {
this.lastInAppUrl = url;
}
}
});
// Check InAppBrowser closed.
CoreEvents.on(CoreEvents.IAB_EXIT, () => {
CoreLoginHelper.setWaitingForBrowser(false);
this.lastInAppUrl = '';
CoreLoginHelper.checkLogout();
});
Platform.resume.subscribe(() => {
// Wait a second before setting it to false since in iOS there could be some frozen WS calls.
setTimeout(() => {
CoreLoginHelper.setWaitingForBrowser(false);
CoreLoginHelper.checkLogout();
}, 1000);
});
// Handle app launched with a certain URL (custom URL scheme).
win.handleOpenURL = (url: string): void => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
NgZone.run(() => {
// First check that the URL hasn't been treated a few seconds ago. Sometimes this function is called more than once.
if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
// Function called more than once, stop.
return;
}
if (!CoreCustomURLSchemes.isCustomURL(url)) {
// Not a custom URL, ignore.
return;
}
this.lastUrls[url] = Date.now();
CoreEvents.trigger(CoreEvents.APP_LAUNCHED_URL, url);
CoreCustomURLSchemes.handleCustomURL(url).catch((error) => {
CoreCustomURLSchemes.treatHandleCustomURLError(error);
});
});
};
// "Expose" CoreWindow.open.
win.openWindowSafely = (url: string, name?: string): void => {
CoreWindow.open(url, name);
};
CoreEvents.on(CoreEvents.LOGIN, async (data: CoreEventSiteData) => { CoreEvents.on(CoreEvents.LOGIN, async (data: CoreEventSiteData) => {
if (data.siteId) { if (data.siteId) {
const site = await CoreSites.getSite(data.siteId); const site = await CoreSites.getSite(data.siteId);

View File

@ -47,12 +47,13 @@ import { CoreFileHelperProvider } from '@services/file-helper';
import { CoreGeolocationProvider } from '@services/geolocation'; import { CoreGeolocationProvider } from '@services/geolocation';
import { CoreNavigatorService } from '@services/navigator'; import { CoreNavigatorService } from '@services/navigator';
import { CoreScreenService } from '@services/screen'; import { CoreScreenService } from '@services/screen';
import { CoreCustomURLSchemesProvider } from '@services/urlschemes';
export const CORE_SERVICES: Type<unknown>[] = [ export const CORE_SERVICES: Type<unknown>[] = [
CoreAppProvider, CoreAppProvider,
CoreConfigProvider, CoreConfigProvider,
CoreCronDelegateService, CoreCronDelegateService,
// @todo CoreCustomURLSchemesProvider, CoreCustomURLSchemesProvider,
CoreDbProvider, CoreDbProvider,
CoreFileHelperProvider, CoreFileHelperProvider,
CoreFileSessionProvider, CoreFileSessionProvider,

View File

@ -23,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreCustomURLSchemes } from '@services/urlschemes';
/** /**
* Directive to open a link in external browser or in the app. * Directive to open a link in external browser or in the app.
@ -111,7 +112,15 @@ export class CoreLinkDirective implements OnInit {
return; return;
} }
// @todo: Custom URL schemes. if (CoreCustomURLSchemes.isCustomURL(href)) {
try {
await CoreCustomURLSchemes.handleCustomURL(href);
} catch (error) {
CoreCustomURLSchemes.treatHandleCustomURLError(error);
}
return;
}
return this.openExternalLink(href, openIn); return this.openExternalLink(href, openIn);
} }

View File

@ -1018,9 +1018,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
const text = await CoreUtils.scanQR(); const text = await CoreUtils.scanQR();
if (text) { if (text) {
this.editorElement?.focus(); // Make sure the editor is focused.
document.execCommand('insertText', false, text); document.execCommand('insertText', false, text);
} }
// this.content.resize(); // Resize content, otherwise the content height becomes 1 for some reason.
} }
/** /**

View File

@ -18,8 +18,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreLoginHelper, CoreLoginHelperProvider } from '@features/login/services/login-helper';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site'; import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
@ -50,7 +49,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
isBrowserSSO = false; isBrowserSSO = false;
isFixedUrlSet = false; isFixedUrlSet = false;
showForgottenPassword = true; showForgottenPassword = true;
showScanQR: boolean; showScanQR = false;
protected siteConfig?: CoreSitePublicConfigResponse; protected siteConfig?: CoreSitePublicConfigResponse;
protected eventThrown = false; protected eventThrown = false;
@ -60,19 +59,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
constructor( constructor(
protected fb: FormBuilder, protected fb: FormBuilder,
) { ) {}
const canScanQR = CoreUtils.canScanQR();
if (canScanQR) {
if (typeof CoreConstants.CONFIG.displayqroncredentialscreen == 'undefined') {
this.showScanQR = CoreLoginHelper.isFixedUrlSet();
} else {
this.showScanQR = !!CoreConstants.CONFIG.displayqroncredentialscreen;
}
} else {
this.showScanQR = false;
}
}
/** /**
* Initialize the component. * Initialize the component.
@ -91,6 +78,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
this.logoUrl = !CoreConstants.CONFIG.forceLoginLogo && CoreNavigator.getRouteParam('logoUrl') || undefined; this.logoUrl = !CoreConstants.CONFIG.forceLoginLogo && CoreNavigator.getRouteParam('logoUrl') || undefined;
this.siteConfig = CoreNavigator.getRouteParam('siteConfig'); this.siteConfig = CoreNavigator.getRouteParam('siteConfig');
this.urlToOpen = CoreNavigator.getRouteParam('urlToOpen'); this.urlToOpen = CoreNavigator.getRouteParam('urlToOpen');
this.showScanQR = CoreLoginHelper.displayQRInCredentialsScreen();
this.credForm = this.fb.group({ this.credForm = this.fb.group({
username: [CoreNavigator.getRouteParam<string>('username') || '', Validators.required], username: [CoreNavigator.getRouteParam<string>('username') || '', Validators.required],
@ -285,37 +273,17 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
/** /**
* Show instructions and scan QR code. * Show instructions and scan QR code.
*/
showInstructionsAndScanQR(): void {
// Show some instructions first.
CoreDomUtils.showAlertWithOptions({
header: Translate.instant('core.login.faqwhereisqrcode'),
message: Translate.instant(
'core.login.faqwhereisqrcodeanswer',
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML },
),
buttons: [
{
text: Translate.instant('core.cancel'),
role: 'cancel',
},
{
text: Translate.instant('core.next'),
handler: (): void => {
this.scanQR();
},
},
],
});
}
/**
* Scan a QR code and put its text in the URL input.
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async scanQR(): Promise<void> { async showInstructionsAndScanQR(): Promise<void> {
// @todo Scan for a QR code. try {
await CoreLoginHelper.showScanQRInstructions();
await CoreLoginHelper.scanQR();
} catch {
// Ignore errors.
}
} }
/** /**

View File

@ -61,6 +61,14 @@
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>
<ng-container *ngIf="showScanQR">
<div class="ion-text-center ion-padding">{{ 'core.login.or' | translate }}</div>
<ion-button expand="block" color="light" class="ion-margin" lines="none" (click)="showInstructionsAndScanQR()">
<ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.scanqr' | translate }}</ion-label>
</ion-button>
</ng-container>
</form> </form>
<!-- Forgotten password option. --> <!-- Forgotten password option. -->

View File

@ -51,6 +51,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
isOAuth = false; isOAuth = false;
isLoggedOut: boolean; isLoggedOut: boolean;
siteId!: string; siteId!: string;
showScanQR = false;
protected page?: string; protected page?: string;
protected pageParams?: Params; protected pageParams?: Params;
@ -82,6 +83,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
this.siteUrl = siteId; this.siteUrl = siteId;
this.page = CoreNavigator.getRouteParam('pageName'); this.page = CoreNavigator.getRouteParam('pageName');
this.pageParams = CoreNavigator.getRouteParam('pageParams'); this.pageParams = CoreNavigator.getRouteParam('pageParams');
this.showScanQR = CoreLoginHelper.displayQRInSiteScreen() || CoreLoginHelper.displayQRInCredentialsScreen();
try { try {
const site = await CoreSites.getSite(this.siteId); const site = await CoreSites.getSite(this.siteId);
@ -246,4 +248,19 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
} }
} }
/**
* Show instructions and scan QR code.
*
* @return Promise resolved when done.
*/
async showInstructionsAndScanQR(): Promise<void> {
try {
await CoreLoginHelper.showScanQRInstructions();
await CoreLoginHelper.scanQR();
} catch {
// Ignore errors.
}
}
} }

View File

@ -30,6 +30,8 @@ import { CoreUrlUtils } from '@services/utils/url';
import { CoreLoginSiteHelpComponent } from '@features/login/components/site-help/site-help'; import { CoreLoginSiteHelpComponent } from '@features/login/components/site-help/site-help';
import { CoreLoginSiteOnboardingComponent } from '@features/login/components/site-onboarding/site-onboarding'; import { CoreLoginSiteOnboardingComponent } from '@features/login/components/site-onboarding/site-onboarding';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services/urlschemes';
import { CoreTextUtils } from '@services/utils/text';
/** /**
* Page that displays a "splash screen" while the app is being initialized. * Page that displays a "splash screen" while the app is being initialized.
@ -82,8 +84,7 @@ export class CoreLoginSitePage implements OnInit {
this.initOnboarding(); this.initOnboarding();
} }
this.showScanQR = CoreUtils.canScanQR() && (typeof CoreConstants.CONFIG.displayqronsitescreen == 'undefined' || this.showScanQR = CoreLoginHelper.displayQRInSiteScreen();
!!CoreConstants.CONFIG.displayqronsitescreen);
this.siteForm = this.formBuilder.group({ this.siteForm = this.formBuilder.group({
siteUrl: [url, this.moodleUrlValidator()], siteUrl: [url, this.moodleUrlValidator()],
@ -464,28 +465,17 @@ export class CoreLoginSitePage implements OnInit {
/** /**
* Show instructions and scan QR code. * Show instructions and scan QR code.
*
* @return Promise resolved when done.
*/ */
showInstructionsAndScanQR(): void { async showInstructionsAndScanQR(): Promise<void> {
// Show some instructions first. try {
CoreDomUtils.showAlertWithOptions({ await CoreLoginHelper.showScanQRInstructions();
header: Translate.instant('core.login.faqwhereisqrcode'),
message: Translate.instant( await this.scanQR();
'core.login.faqwhereisqrcodeanswer', } catch {
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }, // Ignore errors.
), }
buttons: [
{
text: Translate.instant('core.cancel'),
role: 'cancel',
},
{
text: Translate.instant('core.next'),
handler: (): void => {
this.scanQR();
},
},
],
});
} }
/** /**
@ -497,9 +487,83 @@ export class CoreLoginSitePage implements OnInit {
// Scan for a QR code. // Scan for a QR code.
const text = await CoreUtils.scanQR(); const text = await CoreUtils.scanQR();
if (text) { if (!text) {
// @todo return;
} }
if (CoreCustomURLSchemes.isCustomURL(text)) {
try {
await CoreCustomURLSchemes.handleCustomURL(text);
} catch (error) {
if (error && error.data && error.data.isAuthenticationURL && error.data.siteUrl) {
// An error ocurred, but it's an authentication URL and we have the site URL.
this.treatErrorInAuthenticationCustomURL(text, error);
} else {
CoreCustomURLSchemes.treatHandleCustomURLError(error);
}
}
return;
}
// Not a custom URL scheme, check if it's a URL scheme to another app.
const scheme = CoreUrlUtils.getUrlProtocol(text);
if (scheme && scheme != 'http' && scheme != 'https') {
CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text }));
} else if (CoreLoginHelper.isSiteUrlAllowed(text)) {
// Put the text in the field (if present).
this.siteForm.controls.siteUrl.setValue(text);
this.connect(new Event('click'), text);
} else {
CoreDomUtils.showErrorModal('core.errorurlschemeinvalidsite', true);
}
}
/**
* Treat an error while handling a custom URL meant to perform an authentication.
* If the site doesn't use SSO, the user will be sent to the credentials screen.
*
* @param customURL Custom URL handled.
* @param error Error data.
* @return Promise resolved when done.
*/
protected async treatErrorInAuthenticationCustomURL(customURL: string, error: CoreCustomURLSchemesHandleError): Promise<void> {
const siteUrl = error.data?.siteUrl || '';
const modal = await CoreDomUtils.showModalLoading();
// Set the site URL in the input.
this.siteForm.controls.siteUrl.setValue(siteUrl);
try {
// Check if site uses SSO.
const response = await CoreSites.checkSite(siteUrl);
await CoreSites.checkApplication(response);
if (!CoreLoginHelper.isSSOLoginNeeded(response.code)) {
// No SSO, go to credentials page.
await CoreNavigator.navigate('/login/credentials', {
params: {
siteUrl: response.siteUrl,
siteConfig: response.config,
},
});
}
} catch (error) {
// Ignore errors.
} finally {
modal.dismiss();
}
// Now display the error.
error.error = CoreTextUtils.addTextToError(
error.error,
'<br><br>' + Translate.instant('core.login.youcanstillconnectwithcredentials'),
);
CoreCustomURLSchemes.treatHandleCustomURLError(error);
} }
} }

View File

@ -33,6 +33,8 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreCustomURLSchemes } from '@services/urlschemes';
/** /**
* Helper provider that provides some common features regarding authentication. * Helper provider that provides some common features regarding authentication.
@ -53,7 +55,7 @@ export class CoreLoginHelperProvider {
protected isSSOConfirmShown = false; protected isSSOConfirmShown = false;
protected isOpenEditAlertShown = false; protected isOpenEditAlertShown = false;
protected isOpeningReconnect = false; protected isOpeningReconnect = false;
waitingForBrowser = false; protected waitingForBrowser = false;
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreLoginHelper'); this.logger = CoreLogger.getInstance('CoreLoginHelper');
@ -319,7 +321,7 @@ export class CoreLoginHelperProvider {
* @param params Params. * @param params Params.
* @return OAuth ID. * @return OAuth ID.
*/ */
getOAuthIdFromParams(params: CoreUrlParams): number | undefined { getOAuthIdFromParams(params?: CoreUrlParams): number | undefined {
return params && typeof params.oauthsso != 'undefined' ? Number(params.oauthsso) : undefined; return params && typeof params.oauthsso != 'undefined' ? Number(params.oauthsso) : undefined;
} }
@ -1221,6 +1223,110 @@ export class CoreLoginHelperProvider {
} }
} }
/**
* Return whether the app is waiting for browser.
*
* @return Whether the app is waiting for browser.
*/
isWaitingForBrowser(): boolean {
return this.waitingForBrowser;
}
/**
* Set whether the app is waiting for browser.
*
* @param value New value.
*/
setWaitingForBrowser(value: boolean): void {
this.waitingForBrowser = value;
}
/**
* Check whether the QR reader should be displayed in site screen.
*
* @return Whether the QR reader should be displayed in site screen.
*/
displayQRInSiteScreen(): boolean {
return CoreUtils.canScanQR() && (typeof CoreConstants.CONFIG.displayqronsitescreen == 'undefined' ||
!!CoreConstants.CONFIG.displayqronsitescreen);
}
/**
* Check whether the QR reader should be displayed in credentials screen.
*
* @return Whether the QR reader should be displayed in credentials screen.
*/
displayQRInCredentialsScreen(): boolean {
if (!CoreUtils.canScanQR()) {
return false;
}
return (CoreConstants.CONFIG.displayqroncredentialscreen === undefined && this.isFixedUrlSet()) ||
(CoreConstants.CONFIG.displayqroncredentialscreen !== undefined && !!CoreConstants.CONFIG.displayqroncredentialscreen);
}
/**
* Show instructions to scan QR code.
*
* @return Promise resolved if the user accepts to scan QR.
*/
showScanQRInstructions(): Promise<void> {
const deferred = CoreUtils.promiseDefer<void>();
// Show some instructions first.
CoreDomUtils.showAlertWithOptions({
header: Translate.instant('core.login.faqwhereisqrcode'),
message: Translate.instant(
'core.login.faqwhereisqrcodeanswer',
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML },
),
buttons: [
{
text: Translate.instant('core.cancel'),
role: 'cancel',
handler: (): void => {
deferred.reject(new CoreCanceledError());
},
},
{
text: Translate.instant('core.next'),
handler: (): void => {
deferred.resolve();
},
},
],
});
return deferred.promise;
}
/**
* Scan a QR code and tries to authenticate the user using custom URL scheme.
*
* @return Promise resolved when done.
*/
async scanQR(): Promise<void> {
// Scan for a QR code.
const text = await CoreUtils.scanQR();
if (text && CoreCustomURLSchemes.isCustomURL(text)) {
try {
await CoreCustomURLSchemes.handleCustomURL(text);
} catch (error) {
CoreCustomURLSchemes.treatHandleCustomURLError(error);
}
} else if (text) {
// Not a custom URL scheme, check if it's a URL scheme to another app.
const scheme = CoreUrlUtils.getUrlProtocol(text);
if (scheme && scheme != 'http' && scheme != 'https') {
CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text }));
} else {
CoreDomUtils.showErrorModal('core.login.errorqrnoscheme', true);
}
}
}
} }
export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider); export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider);

View File

@ -23,6 +23,10 @@ import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/ma
import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu'; import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreCustomURLSchemes } from '@services/urlschemes';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons';
/** /**
* Page that displays the main menu of the app. * Page that displays the main menu of the app.
@ -154,10 +158,31 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
*/ */
async scanQR(): Promise<void> { async scanQR(): Promise<void> {
// Scan for a QR code. // Scan for a QR code.
// @todo const text = await CoreUtils.scanQR();
// eslint-disable-next-line no-console
console.error('scanQR not implemented');
if (!text) {
return;
}
if (CoreCustomURLSchemes.isCustomURL(text)) {
// Is a custom URL scheme, handle it.
CoreCustomURLSchemes.handleCustomURL(text).catch((error) => {
CoreCustomURLSchemes.treatHandleCustomURLError(error);
});
} else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL.
// Check if the app can handle the URL.
const treated = await CoreContentLinksHelper.handleLink(text, undefined, true, true);
if (!treated) {
// Can't handle it, open it in browser.
CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(text);
}
} else {
// It's not a URL, open it in a modal so the user can see it and copy it.
CoreTextUtils.viewText(Translate.instant('core.qrscanner'), text, {
displayCopyButton: true,
});
}
} }
/** /**

View File

@ -16,11 +16,13 @@ import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreViewerImageComponent } from './image/image'; import { CoreViewerImageComponent } from './image/image';
import { CoreViewerQRScannerComponent } from './qr-scanner/qr-scanner';
import { CoreViewerTextComponent } from './text/text'; import { CoreViewerTextComponent } from './text/text';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreViewerImageComponent, CoreViewerImageComponent,
CoreViewerQRScannerComponent,
CoreViewerTextComponent, CoreViewerTextComponent,
], ],
imports: [ imports: [
@ -28,6 +30,7 @@ import { CoreViewerTextComponent } from './text/text';
], ],
exports: [ exports: [
CoreViewerImageComponent, CoreViewerImageComponent,
CoreViewerQRScannerComponent,
CoreViewerTextComponent, CoreViewerTextComponent,
], ],
}) })

View File

@ -0,0 +1,12 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

View File

@ -0,0 +1,80 @@
// (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, Input, OnDestroy, OnInit } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, Translate } from '@singletons';
/**
* Page to scan a QR code.
*/
@Component({
selector: 'core-viewer-qr-scanner',
templateUrl: 'qr-scanner.html',
})
export class CoreViewerQRScannerComponent implements OnInit, OnDestroy {
@Input() title?: string; // Page title.
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.title = this.title || Translate.instant('core.scanqr');
try {
let text = await CoreUtils.startScanQR();
// Text captured, return it.
text = typeof text == 'string' ? text.trim() : '';
this.closeModal(text);
} catch (error) {
if (!CoreDomUtils.isCanceledError(error)) {
// Show error and stop scanning.
CoreDomUtils.showErrorModalDefault(error, 'An error occurred.');
CoreUtils.stopScanQR();
}
this.closeModal();
}
}
/**
* Cancel scanning.
*/
cancel(): void {
CoreUtils.stopScanQR();
}
/**
* Close modal.
*
* @param text The text to return (if any).
*/
closeModal(text?: string): void {
ModalController.dismiss(text);
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
// If this code is reached and scan hasn't been stopped yet it means the user clicked the back button, cancel.
CoreUtils.stopScanQR();
}
}

View File

@ -498,6 +498,9 @@ export class CoreSitesProvider {
this.currentSite = candidateSite; this.currentSite = candidateSite;
// Store session. // Store session.
this.login(siteId); this.login(siteId);
} else if (this.currentSite && this.currentSite.getId() == siteId) {
// Current site has just been updated, trigger the event.
CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId);
} }
CoreEvents.trigger(CoreEvents.SITE_ADDED, info, siteId); CoreEvents.trigger(CoreEvents.SITE_ADDED, info, siteId);

View File

@ -0,0 +1,564 @@
// (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 { CoreWSError } from '@classes/errors/wserror';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreLoginHelper, CoreLoginSSOData } from '@features/login/services/login-helper';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { ApplicationInit, makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreConstants } from '../constants';
import { CoreApp } from './app';
import { CoreNavigator } from './navigator';
import { CoreSiteCheckResponse, CoreSites } from './sites';
import { CoreDomUtils } from './utils/dom';
import { CoreTextErrorObject, CoreTextUtils } from './utils/text';
import { CoreUrlUtils } from './utils/url';
import { CoreUtils } from './utils/utils';
/*
* Provider to handle custom URL schemes.
*/
@Injectable({ providedIn: 'root' })
export class CoreCustomURLSchemesProvider {
protected logger: CoreLogger;
protected lastUrls: Record<string, number> = {};
constructor() {
this.logger = CoreLogger.getInstance('CoreCustomURLSchemesProvider');
}
/**
* Given some data of a custom URL with a token, create a site if it needs to be created.
*
* @param data URL data.
* @return Promise resolved with the site ID if created or already exists.
*/
protected async createSiteIfNeeded(data: CoreCustomURLSchemesParams): Promise<string | undefined> {
if (!data.token) {
return;
}
const currentSite = CoreSites.getCurrentSite();
if (!currentSite || currentSite.getToken() != data.token) {
// Token belongs to a different site, create it. It doesn't matter if it already exists.
if (!data.siteUrl.match(/^https?:\/\//)) {
// URL doesn't have a protocol and it's required to be able to create the site. Check which one to use.
const result = await CoreSites.checkSite(data.siteUrl);
data.siteUrl = result.siteUrl;
await CoreSites.checkApplication(result);
}
return CoreSites.newSite(
data.siteUrl,
data.token,
data.privateToken,
!!data.isSSOToken,
CoreLoginHelper.getOAuthIdFromParams(data.ssoUrlParams),
);
} else {
// Token belongs to current site, no need to create it.
return CoreSites.getCurrentSiteId();
}
}
/**
* Handle an URL received by custom URL scheme.
*
* @param url URL to treat.
* @return Promise resolved when done. If rejected, the parameter is of type CoreCustomURLSchemesHandleError.
*/
async handleCustomURL(url: string): Promise<void> {
if (!this.isCustomURL(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
/* First check that this URL hasn't been treated a few seconds ago. The function that handles custom URL schemes already
does this, but this function is called from other places so we need to handle it in here too. */
if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
// Function called more than once, stop.
return;
}
this.lastUrls[url] = Date.now();
url = CoreTextUtils.decodeURIComponent(url);
// Wait for app to be ready.
await ApplicationInit.donePromise;
// Some platforms like Windows add a slash at the end. Remove it.
// Some sites add a # at the end of the URL. If it's there, remove it.
url = url.replace(/\/?#?\/?$/, '');
const modal = await CoreDomUtils.showModalLoading();
let data: CoreCustomURLSchemesParams;
// Get the data from the URL.
try {
if (this.isCustomURLToken(url)) {
data = await this.getCustomURLTokenData(url);
} else if (this.isCustomURLLink(url)) {
// In iOS, the protocol after the scheme doesn't have ":". Add it.
url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://');
data = await this.getCustomURLLinkData(url);
} else {
// In iOS, the protocol after the scheme doesn't have ":". Add it.
url = url.replace(/\/\/(https?)\/\//, '//$1://');
data = await this.getCustomURLData(url);
}
} catch (error) {
modal.dismiss();
throw error;
}
try {
const isValid = await CoreLoginHelper.isSiteUrlAllowed(data.siteUrl);
if (!isValid) {
throw Translate.instant('core.errorurlschemeinvalidsite');
}
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
// Redirect URL must belong to the same site. Reject.
throw Translate.instant('core.contentlinks.errorredirectothersite');
}
// First of all, create the site if needed.
const siteId = await this.createSiteIfNeeded(data);
if (data.isSSOToken || (data.isAuthenticationURL && siteId && CoreSites.getCurrentSiteId() == siteId)) {
// Site created and authenticated, open the page to go.
if (data.pageName) {
// Page defined, go to that page instead of site initial page.
CoreNavigator.navigateToSitePath(data.pageName, {
params: data.pageParams,
});
} else {
CoreNavigator.navigateToSiteHome();
}
return;
}
if (data.redirect && !data.redirect.match(/^https?:\/\//)) {
// Redirect is a relative URL. Append the site URL.
data.redirect = CoreTextUtils.concatenatePaths(data.siteUrl, data.redirect);
}
let siteIds = [siteId];
if (!siteId) {
// No site created, check if the site is stored (to know which one to use).
siteIds = await CoreSites.getSiteIdsFromUrl(data.siteUrl, true, data.username);
}
if (siteIds.length > 1) {
// More than one site to treat the URL, let the user choose.
CoreContentLinksHelper.goToChooseSite(data.redirect || data.siteUrl);
} else if (siteIds.length == 1) {
// Only one site, handle the link.
const site = await CoreSites.getSite(siteIds[0]);
if (!data.redirect) {
// No redirect, go to the root URL if needed.
await CoreContentLinksHelper.handleRootURL(site, false, true);
} else {
// Handle the redirect link.
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
/* Always use the username from the site in this case. If the link has a username and a token,
this will make sure that the link is opened with the user the token belongs to. */
const username = site.getInfo()?.username || data.username;
const treated = await CoreContentLinksHelper.handleLink(data.redirect, username);
if (!treated) {
CoreDomUtils.showErrorModal('core.contentlinks.errornoactions', true);
}
}
} else {
// Site not stored. Try to add the site.
const result = await CoreSites.checkSite(data.siteUrl);
// Site exists. We'll allow to add it.
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
await this.goToAddSite(data, result);
}
} catch (error) {
throw new CoreCustomURLSchemesHandleError(error, data);
} finally {
modal.dismiss();
if (data.isSSOToken) {
CoreApp.finishSSOAuthentication();
}
}
}
/**
* Get the data from a custom URL scheme. The structure of the URL is:
* moodlemobile://username@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2
*
* @param url URL to treat.
* @return Promise resolved with the data.
*/
protected async getCustomURLData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURL(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
// App opened using custom URL scheme.
this.logger.debug('Treating custom URL scheme: ' + url);
// Delete the sso scheme from the URL.
url = this.removeCustomURLScheme(url);
// Detect if there's a user specified.
const username = CoreUrlUtils.getUsernameFromUrl(url);
if (username) {
url = url.replace(username + '@', ''); // Remove the username from the URL.
}
// Get the params of the URL.
const params = CoreUrlUtils.extractUrlParams(url);
// Remove the params to get the site URL.
if (url.indexOf('?') != -1) {
url = url.substr(0, url.indexOf('?'));
}
if (!url.match(/https?:\/\//)) {
// Url doesn't have a protocol. Check if the site is stored in the app to be able to determine the protocol.
const siteIds = await CoreSites.getSiteIdsFromUrl(url, true, username);
if (siteIds.length) {
// There is at least 1 site with this URL. Use it to know the full URL.
const site = await CoreSites.getSite(siteIds[0]);
url = site.getURL();
}
}
return {
siteUrl: url,
username: username,
token: params.token,
privateToken: params.privateToken,
redirect: params.redirect,
isAuthenticationURL: !!params.token,
};
}
/**
* Get the data from a "link" custom URL scheme. This kind of URL is deprecated.
*
* @param url URL to treat.
* @return Promise resolved with the data.
*/
protected async getCustomURLLinkData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURLLink(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
// App opened using custom URL scheme.
this.logger.debug('Treating custom URL scheme with link param: ' + url);
// Delete the sso scheme from the URL.
url = this.removeCustomURLLinkScheme(url);
// Detect if there's a user specified.
const username = CoreUrlUtils.getUsernameFromUrl(url);
if (username) {
url = url.replace(username + '@', ''); // Remove the username from the URL.
}
// First of all, check if it's the root URL of a site.
const data = await CoreSites.isStoredRootURL(url, username);
if (data.site) {
// Root URL.
return {
siteUrl: data.site.getURL(),
username: username,
};
} else if (data.siteIds.length > 0) {
// Not the root URL, but at least 1 site supports the URL. Get the site URL from the list of sites.
const site = await CoreSites.getSite(data.siteIds[0]);
return {
siteUrl: site.getURL(),
username: username,
redirect: url,
};
} else {
// Get the site URL.
let siteUrl = CoreContentLinksDelegate.getSiteUrl(url);
let redirect: string | undefined = url;
if (!siteUrl) {
// Site URL not found, use the original URL since it could be the root URL of the site.
siteUrl = url;
redirect = undefined;
}
return {
siteUrl: siteUrl,
username: username,
redirect: redirect,
};
}
}
/**
* Get the data from a "token" custom URL scheme. This kind of URL is deprecated.
*
* @param url URL to treat.
* @return Promise resolved with the data.
*/
protected async getCustomURLTokenData(url: string): Promise<CoreCustomURLSchemesParams> {
if (!this.isCustomURLToken(url)) {
throw new CoreCustomURLSchemesHandleError(null);
}
if (CoreApp.isSSOAuthenticationOngoing()) {
// Authentication ongoing, probably duplicated request.
throw new CoreCustomURLSchemesHandleError('Duplicated');
}
// App opened using custom URL scheme. Probably an SSO authentication.
CoreApp.startSSOAuthentication();
this.logger.debug('App launched by URL with an SSO');
// Delete the sso scheme from the URL.
url = this.removeCustomURLTokenScheme(url);
// Some platforms like Windows add a slash at the end. Remove it.
// Some sites add a # at the end of the URL. If it's there, remove it.
url = url.replace(/\/?#?\/?$/, '');
// Decode from base64.
try {
url = atob(url);
} catch (err) {
// Error decoding the parameter.
this.logger.error('Error decoding parameter received for login SSO');
throw new CoreCustomURLSchemesHandleError(null);
}
const data: CoreCustomURLSchemesParams = await CoreLoginHelper.validateBrowserSSOLogin(url);
data.isSSOToken = true;
data.isAuthenticationURL = true;
return data;
}
/**
* Go to page to add a site, or open a browser if SSO.
*
* @param data URL data.
* @param checkResponse Result of checkSite.
* @return Promise resolved when done.
*/
protected async goToAddSite(data: CoreCustomURLSchemesParams, checkResponse: CoreSiteCheckResponse): Promise<void> {
const ssoNeeded = CoreLoginHelper.isSSOLoginNeeded(checkResponse.code);
const pageParams = {
siteUrl: checkResponse.siteUrl,
username: data.username,
urlToOpen: data.redirect,
siteConfig: checkResponse.config,
};
let hasSitePluginsLoaded = false;
if (CoreSites.isLoggedIn()) {
// Ask the user before changing site.
await CoreDomUtils.showConfirm(Translate.instant('core.contentlinks.confirmurlothersite'));
if (!ssoNeeded) {
hasSitePluginsLoaded = CoreSitePlugins.hasSitePluginsLoaded;
if (hasSitePluginsLoaded) {
// Store the redirect since logout will restart the app.
CoreApp.storeRedirect(CoreConstants.NO_SITE_ID, '/login/credentials', pageParams);
}
await CoreSites.logout();
}
}
if (ssoNeeded) {
CoreLoginHelper.confirmAndOpenBrowserForSSOLogin(
checkResponse.siteUrl,
checkResponse.code,
checkResponse.service,
checkResponse.config?.launchurl,
);
} else if (!hasSitePluginsLoaded) {
await CoreNavigator.navigateToLoginCredentials(pageParams);
}
}
/**
* Check whether a URL is a custom URL scheme.
*
* @param url URL to check.
* @return Whether it's a custom URL scheme.
*/
isCustomURL(url: string): boolean {
if (!url) {
return false;
}
return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://') != -1;
}
/**
* Check whether a URL is a custom URL scheme with the "link" param (deprecated).
*
* @param url URL to check.
* @return Whether it's a custom URL scheme.
*/
isCustomURLLink(url: string): boolean {
if (!url) {
return false;
}
return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://link=') != -1;
}
/**
* Check whether a URL is a custom URL scheme with a "token" param (deprecated).
*
* @param url URL to check.
* @return Whether it's a custom URL scheme.
*/
isCustomURLToken(url: string): boolean {
if (!url) {
return false;
}
return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://token=') != -1;
}
/**
* Remove the scheme from a custom URL.
*
* @param url URL to treat.
* @return URL without scheme.
*/
removeCustomURLScheme(url: string): string {
return url.replace(CoreConstants.CONFIG.customurlscheme + '://', '');
}
/**
* Remove the scheme and the "link=" prefix from a link custom URL.
*
* @param url URL to treat.
* @return URL without scheme and prefix.
*/
removeCustomURLLinkScheme(url: string): string {
return url.replace(CoreConstants.CONFIG.customurlscheme + '://link=', '');
}
/**
* Remove the scheme and the "token=" prefix from a token custom URL.
*
* @param url URL to treat.
* @return URL without scheme and prefix.
*/
removeCustomURLTokenScheme(url: string): string {
return url.replace(CoreConstants.CONFIG.customurlscheme + '://token=', '');
}
/**
* Treat error returned by handleCustomURL.
*
* @param error Error data.
*/
treatHandleCustomURLError(error: CoreCustomURLSchemesHandleError): void {
if (error.error == 'Duplicated') {
// Duplicated request
} else if (CoreUtils.isWebServiceError(error.error) && error.data && error.data.isSSOToken) {
// An error occurred, display the error and logout the user.
CoreLoginHelper.treatUserTokenError(error.data.siteUrl, <CoreWSError> error.error);
CoreSites.logout();
} else {
CoreDomUtils.showErrorModalDefault(error.error, Translate.instant('core.login.invalidsite'));
}
}
}
/**
* Error returned by handleCustomURL.
*/
export class CoreCustomURLSchemesHandleError extends CoreError {
/**
* Constructor.
*
* @param error The error message or object.
* @param data Data obtained from the URL (if any).
*/
constructor(public error: string | CoreError | CoreTextErrorObject | null, public data?: CoreCustomURLSchemesParams) {
super(CoreTextUtils.getErrorMessageFromError(error));
}
}
export const CoreCustomURLSchemes = makeSingleton(CoreCustomURLSchemesProvider);
/**
* All params that can be in a custom URL scheme.
*/
export interface CoreCustomURLSchemesParams extends CoreLoginSSOData {
/**
* Username.
*/
username?: string;
/**
* URL to open once authenticated.
*/
redirect?: string;
/**
* Whether it's an SSO token URL.
*/
isSSOToken?: boolean;
/**
* Whether the URL is meant to perform an authentication.
*/
isAuthenticationURL?: boolean;
}

View File

@ -118,12 +118,21 @@ export class CoreTextUtilsProvider {
* @param text Text to add. * @param text Text to add.
* @return Modified error. * @return Modified error.
*/ */
addTextToError(error: string | CoreTextErrorObject, text: string): string | CoreTextErrorObject { addTextToError(error: string | CoreError | CoreTextErrorObject | undefined | null, text: string): string | CoreTextErrorObject {
if (typeof error == 'string') { if (typeof error == 'string') {
return error + text; return error + text;
} }
if (error) { if (error instanceof CoreError) {
error.message += text;
return error;
}
if (!error) {
return text;
}
if (typeof error.message == 'string') { if (typeof error.message == 'string') {
error.message += text; error.message += text;
} else if (typeof error.error == 'string') { } else if (typeof error.error == 'string') {
@ -133,7 +142,6 @@ export class CoreTextUtilsProvider {
} else if (typeof error.body == 'string') { } else if (typeof error.body == 'string') {
error.body += text; error.body += text;
} }
}
return error; return error;
} }
@ -522,10 +530,10 @@ export class CoreTextUtilsProvider {
/** /**
* Get the error message from an error object. * Get the error message from an error object.
* *
* @param error Error object. * @param error Error.
* @return Error message, undefined if not found. * @return Error message, undefined if not found.
*/ */
getErrorMessageFromError(error?: string | CoreError | CoreTextErrorObject): string | undefined { getErrorMessageFromError(error?: string | CoreError | CoreTextErrorObject | null): string | undefined {
if (typeof error == 'string') { if (typeof error == 'string') {
return error; return error;
} }
@ -534,7 +542,11 @@ export class CoreTextUtilsProvider {
return error.message; return error.message;
} }
return error && (error.message || error.error || error.content || error.body); if (!error) {
return undefined;
}
return error.message || error.error || error.content || error.body;
} }
/** /**

View File

@ -362,7 +362,7 @@ export class CoreUrlUtilsProvider {
* @param url URL to treat. * @param url URL to treat.
* @return Username. Undefined if no username found. * @return Username. Undefined if no username found.
*/ */
getUsernameFromUrl(url: string): string | void { getUsernameFromUrl(url: string): string | undefined {
if (url.indexOf('@') > -1) { if (url.indexOf('@') > -1) {
// Get URL without protocol. // Get URL without protocol.
const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); const withoutProtocol = url.replace(/^[^?@/]*:\/\//, '');

View File

@ -26,9 +26,11 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreWSError } from '@classes/errors/wserror'; import { CoreWSError } from '@classes/errors/wserror';
import { makeSingleton, Clipboard, InAppBrowser, FileOpener, WebIntent, QRScanner, Translate } from '@singletons'; import { makeSingleton, Clipboard, InAppBrowser, FileOpener, WebIntent, QRScanner, Translate, ModalController } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreFileSizeSum } from '@services/plugin-file-delegate';
import { CoreViewerQRScannerComponent } from '@features/viewer/components/qr-scanner/qr-scanner';
import { CoreCanceledError } from '@classes/errors/cancelederror';
type TreeNode<T> = T & { children: TreeNode<T>[] }; type TreeNode<T> = T & { children: TreeNode<T>[] };
@ -1489,12 +1491,20 @@ export class CoreUtilsProvider {
* @param title Title of the modal. Defaults to "QR reader". * @param title Title of the modal. Defaults to "QR reader".
* @return Promise resolved with the captured text or undefined if cancelled or error. * @return Promise resolved with the captured text or undefined if cancelled or error.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars async scanQR(title?: string): Promise<string> {
scanQR(title?: string): Promise<string> { const modal = await ModalController.create({
// eslint-disable-next-line @typescript-eslint/no-unused-vars component: CoreViewerQRScannerComponent,
return new Promise((resolve, reject): void => { cssClass: 'core-modal-fullscreen',
// @todo componentProps: {
title,
},
}); });
await modal.present();
const result = await modal.onWillDismiss();
return result.data;
} }
/** /**
@ -1503,12 +1513,6 @@ export class CoreUtilsProvider {
* @return Promise resolved with the QR string, rejected if error or cancelled. * @return Promise resolved with the QR string, rejected if error or cancelled.
*/ */
async startScanQR(): Promise<string | undefined> { async startScanQR(): Promise<string | undefined> {
try {
return this.startScanQR();
} catch (error) {
// do nothing
}
if (!CoreApp.isMobile()) { if (!CoreApp.isMobile()) {
return Promise.reject('QRScanner isn\'t available in browser.'); return Promise.reject('QRScanner isn\'t available in browser.');
} }
@ -1580,7 +1584,7 @@ export class CoreUtilsProvider {
} else if (typeof data != 'undefined') { } else if (typeof data != 'undefined') {
this.qrScanData.deferred.resolve(data as string); this.qrScanData.deferred.resolve(data as string);
} else { } else {
this.qrScanData.deferred.reject(CoreDomUtils.createCanceledError()); this.qrScanData.deferred.reject(new CoreCanceledError());
} }
delete this.qrScanData; delete this.qrScanData;

View File

@ -45,6 +45,7 @@ export interface CoreEventsData {
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
}; };
/* /*

View File

@ -454,3 +454,14 @@ ion-button.core-button-select {
.core-iframe-offline-disabled { .core-iframe-offline-disabled {
display: none !important; display: none !important;
} }
.core-scanning-qr {
.ion-page, .modal-wrapper {
background-color: transparent !important;
--background: transparent;
}
ion-content, ion-backdrop, ion-modal:not(.core-modal-fullscreen), ion-tabs {
display: none !important;
}
}