diff --git a/src/core/core.module.ts b/src/core/core.module.ts index 214cafe38..28ba4326b 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -58,6 +58,7 @@ export async function getCoreServices(): Promise[]> { const { CoreUtilsProvider } = await import('@services/utils/utils'); const { CoreWSProvider } = await import('@services/ws'); const { CorePlatformService } = await import('@services/platform'); + const { CoreQRScanService } = await import('@services/qrscan'); return [ CoreAppProvider, @@ -80,6 +81,7 @@ export async function getCoreServices(): Promise[]> { CorePluginFileDelegateService, CorePopoversService, CorePlatformService, + CoreQRScanService, CoreScreenService, CoreSitesProvider, CoreSyncProvider, diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index 5e4fb95ea..b91a1576a 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -48,6 +48,7 @@ import { CoreSwiper } from '@singletons/swiper'; import { CoreTextUtils } from '@services/utils/text'; import { CoreWait } from '@singletons/wait'; import { toBoolean } from '@/core/transforms/boolean'; +import { CoreQRScan } from '@services/qrscan'; /** * Component to display a rich text editor if enabled. @@ -182,7 +183,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @inheritdoc */ ngOnInit(): void { - this.canScanQR = CoreUtils.canScanQR(); + this.canScanQR = CoreQRScan.canScanQR(); this.isPhone = CoreScreen.isMobile; this.toolbarHidden = this.isPhone; } @@ -1033,7 +1034,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.stopBubble(event); // Scan for a QR code. - const text = await CoreUtils.scanQR(); + const text = await CoreQRScan.scanQR(); if (text) { this.focusRTE(event); // Make sure the editor is focused. diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index 75e9efb56..99cd87cb3 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -14,7 +14,7 @@ import { AfterViewInit, Component, ElementRef, HostBinding, OnDestroy } from '@angular/core'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreQRScan } from '@services/qrscan'; import { ModalController, Translate } from '@singletons'; import { FAQ_QRCODE_IMAGE_HTML, FAQ_URL_IMAGE_HTML, GET_STARTED_URL } from '@features/login/constants'; import { CoreDomUtils } from '@services/utils/dom'; @@ -45,7 +45,7 @@ export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy { constructor(protected el: ElementRef) { const getStartedTitle = Translate.instant('core.login.faqsetupsitelinktitle'); - const canScanQR = CoreUtils.canScanQR(); + const canScanQR = CoreQRScan.canScanQR(); const urlImageHtml = FAQ_URL_IMAGE_HTML; const qrCodeImageHtml = FAQ_QRCODE_IMAGE_HTML; const setupLinkHtml = `${GET_STARTED_URL}`; diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 49202fd68..327202357 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -47,6 +47,7 @@ import { ONBOARDING_DONE } from '@features/login/constants'; import { CoreUnauthenticatedSite } from '@classes/sites/unauthenticated-site'; import { CoreKeyboard } from '@singletons/keyboard'; import { CoreModals } from '@services/modals'; +import { CoreQRScan } from '@services/qrscan'; /** * Site (url) chooser when adding a new site. @@ -543,7 +544,7 @@ export class CoreLoginSitePage implements OnInit { */ async scanQR(): Promise { // Scan for a QR code. - const text = await CoreUtils.scanQR(); + const text = await CoreQRScan.scanQR(); if (!text) { return; diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 8a92e8108..3bb4c55e7 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -33,7 +33,6 @@ import { CoreLogger } from '@singletons/logger'; import { CoreUrl, CoreUrlParams } from '@singletons/url'; import { CoreNavigator, CoreRedirectPayload } from '@services/navigator'; import { CoreCanceledError } from '@classes/errors/cancelederror'; -import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; @@ -59,6 +58,7 @@ import { } from '../constants'; import { LazyRoutesModule } from '@/app/app-routing.module'; import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror'; +import { CoreQRScan } from '@services/qrscan'; /** * Helper provider that provides some common features regarding authentication. @@ -1271,7 +1271,7 @@ export class CoreLoginHelperProvider { * @returns Whether the QR reader should be displayed in site screen. */ displayQRInSiteScreen(): boolean { - return CoreUtils.canScanQR() && (CoreConstants.CONFIG.displayqronsitescreen === undefined || + return CoreQRScan.canScanQR() && (CoreConstants.CONFIG.displayqronsitescreen === undefined || !!CoreConstants.CONFIG.displayqronsitescreen); } @@ -1282,7 +1282,7 @@ export class CoreLoginHelperProvider { * @returns Whether the QR reader should be displayed in credentials screen. */ async displayQRInCredentialsScreen(qrCodeType = CoreSiteQRCodeType.QR_CODE_LOGIN): Promise { - if (!CoreUtils.canScanQR()) { + if (!CoreQRScan.canScanQR()) { return false; } @@ -1340,23 +1340,19 @@ export class CoreLoginHelperProvider { */ async scanQR(): Promise { // Scan for a QR code. - const text = await CoreUtils.scanQR(); + const text = await CoreQRScan.scanQRWithUrlHandling(); - 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 = CoreUrl.getUrlProtocol(text); + if (!text) { + return; + } - if (scheme && scheme != 'http' && scheme != 'https') { - CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text })); - } else { - CoreDomUtils.showErrorModal('core.login.errorqrnoscheme', true); - } + // Not a custom URL scheme, check if it's a URL scheme to another app. + const scheme = CoreUrl.getUrlProtocol(text); + + if (scheme && scheme != 'http' && scheme != 'https') { + CoreDomUtils.showErrorModal(Translate.instant('core.errorurlschemeinvalidscheme', { $a: text })); + } else { + CoreDomUtils.showErrorModal('core.login.errorqrnoscheme', true); } } diff --git a/src/core/features/mainmenu/pages/more/more.ts b/src/core/features/mainmenu/pages/more/more.ts index afa15de98..5571d74d8 100644 --- a/src/core/features/mainmenu/pages/more/more.ts +++ b/src/core/features/mainmenu/pages/more/more.ts @@ -16,13 +16,11 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { CoreSites } from '@services/sites'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreQRScan } from '@services/qrscan'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate'; import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreNavigator } from '@services/navigator'; -import { CoreCustomURLSchemes } from '@services/urlschemes'; -import { CoreTextUtils } from '@services/utils/text'; import { Translate } from '@singletons'; import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; import { CoreDom } from '@singletons/dom'; @@ -58,7 +56,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { this.loadCustomMenuItems(); - this.showScanQR = CoreUtils.canScanQR() && + this.showScanQR = CoreQRScan.canScanQR() && !CoreSites.getCurrentSite()?.isFeatureDisabled('CoreMainMenuDelegate_QrReader'); } @@ -149,18 +147,14 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { */ async scanQR(): Promise { // Scan for a QR code. - const text = await CoreUtils.scanQR(); + const text = await CoreQRScan.scanQRWithUrlHandling(); 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 it's a URL. + if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { await CoreSites.visitLink(text, { checkRoot: true, openBrowserRoot: true, diff --git a/src/core/features/viewer/components/qr-scanner/qr-scanner.ts b/src/core/features/viewer/components/qr-scanner/qr-scanner.ts index 25e6ac9c0..61a0cfc2e 100644 --- a/src/core/features/viewer/components/qr-scanner/qr-scanner.ts +++ b/src/core/features/viewer/components/qr-scanner/qr-scanner.ts @@ -14,8 +14,8 @@ import { CoreSharedModule } from '@/core/shared.module'; import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { CoreQRScan } from '@services/qrscan'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; import { ModalController, Translate } from '@singletons'; /** @@ -41,17 +41,17 @@ export class CoreViewerQRScannerComponent implements OnInit, OnDestroy { try { - let text = await CoreUtils.startScanQR(); + let text = await CoreQRScan.startScanQR(); // Text captured, return it. - text = typeof text == 'string' ? text.trim() : ''; + 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(); + CoreQRScan.stopScanQR(); } this.closeModal(); @@ -62,7 +62,7 @@ export class CoreViewerQRScannerComponent implements OnInit, OnDestroy { * Cancel scanning. */ cancel(): void { - CoreUtils.stopScanQR(); + CoreQRScan.stopScanQR(); } /** @@ -79,7 +79,7 @@ export class CoreViewerQRScannerComponent implements OnInit, OnDestroy { */ 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(); + CoreQRScan.stopScanQR(); } } diff --git a/src/core/services/qrscan.ts b/src/core/services/qrscan.ts new file mode 100644 index 000000000..257781624 --- /dev/null +++ b/src/core/services/qrscan.ts @@ -0,0 +1,185 @@ +// (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 { makeSingleton } from '@singletons'; +import { CoreCanceledError } from '@classes/errors/cancelederror'; +import { CorePromisedValue } from '@classes/promised-value'; +import { QRScanner } from '@features/native/plugins'; +import { CoreModals } from './modals'; +import { CorePlatform } from './platform'; +import { Subscription } from 'rxjs'; +import { CoreCustomURLSchemes } from './urlschemes'; + +/** + * Handles qr scan services. + */ +@Injectable({ providedIn: 'root' }) +export class CoreQRScanService { + + protected qrScanData?: {deferred: CorePromisedValue; observable: Subscription}; + protected initialColorSchemeContent = 'light dark'; + + /** + * Check whether the app can scan QR codes. + * + * @returns Whether the app can scan QR codes. + */ + canScanQR(): boolean { + return CorePlatform.isMobile(); + } + + /** + * Open a modal to scan a QR code. + * + * @param title Title of the modal. Defaults to "QR reader". + * @returns Promise resolved with the captured text or undefined if cancelled or error. + */ + async scanQR(title?: string): Promise { + const { CoreViewerQRScannerComponent } = await import('@features/viewer/components/qr-scanner/qr-scanner'); + + return CoreModals.openModal({ + component: CoreViewerQRScannerComponent, + cssClass: 'core-modal-fullscreen', + componentProps: { + title, + }, + }); + } + + /** + * Scan a QR code and handle the URL if it's a custom URL scheme. + * + * @param title Title of the modal. Defaults to "QR reader". + * @returns Promise resolved with the captured text or undefined if cancelled, error or URL handled. + */ + async scanQRWithUrlHandling(title?: string): Promise { + // Scan for a QR code. + const text = await CoreQRScan.scanQR(title); + + if (!text) { + return; + } + + if (CoreCustomURLSchemes.isCustomURL(text)) { + // Is a custom URL scheme, handle it. + try { + await CoreCustomURLSchemes.handleCustomURL(text); + } catch (error) { + CoreCustomURLSchemes.treatHandleCustomURLError(error); + } + + return; + } + + return text; + } + + /** + * Start scanning for a QR code. + * + * @returns Promise resolved with the QR string, rejected if error or cancelled. + */ + async startScanQR(): Promise { + if (!CorePlatform.isMobile()) { + return Promise.reject('QRScanner isn\'t available in browser.'); + } + + // Ask the user for permission to use the camera. + // The scan method also does this, but since it returns an Observable we wouldn't be able to detect if the user denied. + try { + const status = await QRScanner.prepare(); + + if (!status.authorized) { + // No access to the camera, reject. In android this shouldn't happen, denying access passes through catch. + throw new Error('The user denied camera access.'); + } + + if (this.qrScanData && this.qrScanData.deferred) { + // Already scanning. + return this.qrScanData.deferred; + } + + // Start scanning. + this.qrScanData = { + deferred: new CorePromisedValue(), + + // When text is received, stop scanning and return the text. + observable: QRScanner.scan().subscribe(text => this.stopScanQR(text, false)), + }; + + // Show the camera. + try { + await QRScanner.show(); + + document.body.classList.add('core-scanning-qr'); + + // Set color-scheme to 'normal', otherwise the camera isn't seen in Android. + const colorSchemeMeta = document.querySelector('meta[name="color-scheme"]'); + if (colorSchemeMeta) { + this.initialColorSchemeContent = colorSchemeMeta.getAttribute('content') || this.initialColorSchemeContent; + colorSchemeMeta.setAttribute('content', 'normal'); + } + + return this.qrScanData.deferred; + } catch (error) { + this.stopScanQR(error, true); + + throw error; + } + } catch (error) { + // eslint-disable-next-line @typescript-eslint/naming-convention + error.message = error.message || (error as { _message?: string })._message; + + throw error; + } + } + + /** + * Stop scanning for QR code. If no param is provided, the app will consider the user cancelled. + * + * @param data If success, the text of the QR code. If error, the error object or message. Undefined for cancelled. + * @param error True if the data belongs to an error, false otherwise. + */ + stopScanQR(data?: string | Error, error?: boolean): void { + if (!this.qrScanData) { + // Not scanning. + return; + } + + // Hide camera preview. + document.body.classList.remove('core-scanning-qr'); + + // Set color-scheme to the initial value. + document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', this.initialColorSchemeContent); + + QRScanner.hide(); + QRScanner.destroy(); + + this.qrScanData.observable.unsubscribe(); // Stop scanning. + + if (error) { + this.qrScanData.deferred.reject(typeof data === 'string' ? new Error(data) : data); + } else if (data !== undefined) { + this.qrScanData.deferred.resolve(data as string); + } else { + this.qrScanData.deferred.reject(new CoreCanceledError()); + } + + delete this.qrScanData; + } + +} + +export const CoreQRScan = makeSingleton(CoreQRScanService); diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 9a18d59a7..ce27cb681 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -15,22 +15,18 @@ import { Injectable } from '@angular/core'; import { InAppBrowserObject, InAppBrowserOptions } from '@awesome-cordova-plugins/in-app-browser'; import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; -import { Subscription } from 'rxjs'; import { CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; import { CoreLang } from '@services/lang'; import { CoreWS } from '@services/ws'; -import { CoreModals } from '@services/modals'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { makeSingleton, InAppBrowser, FileOpener, WebIntent, Translate, NgZone } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileEntry } from '@services/file-helper'; import { CoreConstants } from '@/core/constants'; import { CoreWindow } from '@singletons/window'; import { CoreColors } from '@singletons/colors'; -import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreErrorWithOptions } from '@classes/errors/errorwithoptions'; import { CoreFilepool } from '@services/filepool'; @@ -38,10 +34,10 @@ import { CoreSites } from '@services/sites'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreUrl } from '@singletons/url'; -import { QRScanner } from '@features/native/plugins'; import { CoreArray } from '@singletons/array'; import { CoreText } from '@singletons/text'; import { CoreWait, CoreWaitOptions } from '@singletons/wait'; +import { CoreQRScan } from '@services/qrscan'; export type TreeNode = T & { children: TreeNode[] }; @@ -56,8 +52,6 @@ export class CoreUtilsProvider { protected logger: CoreLogger; protected iabInstance?: InAppBrowserObject; protected uniqueIds: {[name: string]: number} = {}; - protected qrScanData?: {deferred: CorePromisedValue; observable: Subscription}; - protected initialColorSchemeContent = 'light dark'; constructor() { this.logger = CoreLogger.getInstance('CoreUtilsProvider'); @@ -1641,9 +1635,11 @@ export class CoreUtilsProvider { * Check whether the app can scan QR codes. * * @returns Whether the app can scan QR codes. + * + * @deprecated since 4.5. Use CoreQRScan.canScanQR instead. */ canScanQR(): boolean { - return CorePlatform.isMobile(); + return CoreQRScan.canScanQR(); } /** @@ -1651,77 +1647,22 @@ export class CoreUtilsProvider { * * @param title Title of the modal. Defaults to "QR reader". * @returns Promise resolved with the captured text or undefined if cancelled or error. + * + * @deprecated since 4.5. Use CoreQRScan.scanQR instead. */ async scanQR(title?: string): Promise { - const { CoreViewerQRScannerComponent } = await import('@features/viewer/components/qr-scanner/qr-scanner'); - - return CoreModals.openModal({ - component: CoreViewerQRScannerComponent, - cssClass: 'core-modal-fullscreen', - componentProps: { - title, - }, - }); + return CoreQRScan.scanQR(title); } /** * Start scanning for a QR code. * * @returns Promise resolved with the QR string, rejected if error or cancelled. + * + * @deprecated since 4.5. Use CoreQRScan.startScanQR instead. */ async startScanQR(): Promise { - if (!CorePlatform.isMobile()) { - return Promise.reject('QRScanner isn\'t available in browser.'); - } - - // Ask the user for permission to use the camera. - // The scan method also does this, but since it returns an Observable we wouldn't be able to detect if the user denied. - try { - const status = await QRScanner.prepare(); - - if (!status.authorized) { - // No access to the camera, reject. In android this shouldn't happen, denying access passes through catch. - throw new Error('The user denied camera access.'); - } - - if (this.qrScanData && this.qrScanData.deferred) { - // Already scanning. - return this.qrScanData.deferred; - } - - // Start scanning. - this.qrScanData = { - deferred: new CorePromisedValue(), - - // When text is received, stop scanning and return the text. - observable: QRScanner.scan().subscribe(text => this.stopScanQR(text, false)), - }; - - // Show the camera. - try { - await QRScanner.show(); - - document.body.classList.add('core-scanning-qr'); - - // Set color-scheme to 'normal', otherwise the camera isn't seen in Android. - const colorSchemeMeta = document.querySelector('meta[name="color-scheme"]'); - if (colorSchemeMeta) { - this.initialColorSchemeContent = colorSchemeMeta.getAttribute('content') || this.initialColorSchemeContent; - colorSchemeMeta.setAttribute('content', 'normal'); - } - - return this.qrScanData.deferred; - } catch (e) { - this.stopScanQR(e, true); - - throw e; - } - } catch (error) { - // eslint-disable-next-line @typescript-eslint/naming-convention - error.message = error.message || (error as { _message?: string })._message; - - throw error; - } + return CoreQRScan.startScanQR(); } /** @@ -1729,33 +1670,11 @@ export class CoreUtilsProvider { * * @param data If success, the text of the QR code. If error, the error object or message. Undefined for cancelled. * @param error True if the data belongs to an error, false otherwise. + * + * @deprecated since 4.5. Use CoreQRScan.stopScanQR instead. */ stopScanQR(data?: string | Error, error?: boolean): void { - if (!this.qrScanData) { - // Not scanning. - return; - } - - // Hide camera preview. - document.body.classList.remove('core-scanning-qr'); - - // Set color-scheme to the initial value. - document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', this.initialColorSchemeContent); - - QRScanner.hide(); - QRScanner.destroy(); - - this.qrScanData.observable.unsubscribe(); // Stop scanning. - - if (error) { - this.qrScanData.deferred.reject(typeof data === 'string' ? new Error(data) : data); - } else if (data !== undefined) { - this.qrScanData.deferred.resolve(data as string); - } else { - this.qrScanData.deferred.reject(new CoreCanceledError()); - } - - delete this.qrScanData; + CoreQRScan.stopScanQR(data, error); } /**