MOBILE-3712 core: Implement and use QR scanner
parent
e4f719957e
commit
0bbeb991ae
|
@ -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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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. -->
|
||||||
|
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -118,21 +118,29 @@ 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) {
|
||||||
if (typeof error.message == 'string') {
|
error.message += text;
|
||||||
error.message += text;
|
|
||||||
} else if (typeof error.error == 'string') {
|
return error;
|
||||||
error.error += text;
|
}
|
||||||
} else if (typeof error.content == 'string') {
|
|
||||||
error.content += text;
|
if (!error) {
|
||||||
} else if (typeof error.body == 'string') {
|
return text;
|
||||||
error.body += text;
|
}
|
||||||
}
|
|
||||||
|
if (typeof error.message == 'string') {
|
||||||
|
error.message += text;
|
||||||
|
} else if (typeof error.error == 'string') {
|
||||||
|
error.error += text;
|
||||||
|
} else if (typeof error.content == 'string') {
|
||||||
|
error.content += text;
|
||||||
|
} else if (typeof error.body == 'string') {
|
||||||
|
error.body += text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue