MOBILE-4616 viewer: Create a service to have all the qr scan functions

main
Pau Ferrer Ocaña 2024-07-18 15:10:47 +02:00
parent 1412a5571c
commit 73f6a0e6b9
9 changed files with 232 additions and 134 deletions

View File

@ -58,6 +58,7 @@ export async function getCoreServices(): Promise<Type<unknown>[]> {
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<Type<unknown>[]> {
CorePluginFileDelegateService,
CorePopoversService,
CorePlatformService,
CoreQRScanService,
CoreScreenService,
CoreSitesProvider,
CoreSyncProvider,

View File

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

View File

@ -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<HTMLElement>) {
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 = `<a href="${GET_STARTED_URL}" title="${getStartedTitle}">${GET_STARTED_URL}</a>`;

View File

@ -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<void> {
// Scan for a QR code.
const text = await CoreUtils.scanQR();
const text = await CoreQRScan.scanQR();
if (!text) {
return;

View File

@ -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<boolean> {
if (!CoreUtils.canScanQR()) {
if (!CoreQRScan.canScanQR()) {
return false;
}
@ -1340,23 +1340,19 @@ export class CoreLoginHelperProvider {
*/
async scanQR(): Promise<void> {
// 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);
}
}

View File

@ -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<void> {
// 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,

View File

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

View File

@ -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<string>; 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<string | undefined> {
const { CoreViewerQRScannerComponent } = await import('@features/viewer/components/qr-scanner/qr-scanner');
return CoreModals.openModal<string>({
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<string | undefined> {
// 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<string | undefined> {
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);

View File

@ -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> = T & { children: TreeNode<T>[] };
@ -56,8 +52,6 @@ export class CoreUtilsProvider {
protected logger: CoreLogger;
protected iabInstance?: InAppBrowserObject;
protected uniqueIds: {[name: string]: number} = {};
protected qrScanData?: {deferred: CorePromisedValue<string>; 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<string | undefined> {
const { CoreViewerQRScannerComponent } = await import('@features/viewer/components/qr-scanner/qr-scanner');
return CoreModals.openModal<string>({
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<string | undefined> {
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);
}
/**