diff --git a/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.html b/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.html index e8ef82bbd..d0cb96671 100644 --- a/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.html +++ b/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.html @@ -1,16 +1,43 @@ @if (modalRef.data.subtitle; as subtitle) {

{{ subtitle }}

} -@if (modalRef.data.content; as content) { -

- {{ content }} -

+ + +@if (shouldShowQrCode(); as showQr) { + @if (parsedContent(); as parsed) { + @if (parsed.textBefore) { +

{{ parsed.textBefore }}

+ } + +
+ +
+ + @if (parsed.textAfter) { +

{{ parsed.textAfter }}

+ } + } +} @else { + + @if (modalRef.data.content; as content) { +

+ {{ content }} +

+ } } @if (modalRef.data.actions; as actions) {
@for (action of actions; track action) { - } diff --git a/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.scss b/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.scss index f91cfdd15..727e485eb 100644 --- a/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.scss +++ b/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.scss @@ -15,11 +15,15 @@ @apply text-lg text-center whitespace-pre-wrap mb-8 px-16; } +.qr-code-container { + @apply flex flex-col items-center justify-center mb-8; +} + .actions { @apply text-center mb-8; button { - @apply border-2 border-solid border-brand bg-white text-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap ml-4; + @apply border-2 border-solid border-brand bg-white text-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap; &.selected { @apply bg-brand text-white; diff --git a/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.ts b/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.ts index 431753079..96fb03cd8 100644 --- a/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.ts +++ b/apps/isa-app/src/ui/modal/dialog/dialog-modal.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, computed, OnInit } from '@angular/core'; import { CommandService } from '@core/command'; import { UiModalRef } from '../defs/modal-ref'; import { DialogModel } from './dialog.model'; +import { parseDialogContentForUrl } from './dialog.helper'; @Component({ selector: 'ui-dialog-modal', @@ -10,6 +11,26 @@ import { DialogModel } from './dialog.model'; standalone: false, }) export class UiDialogModalComponent implements OnInit { + /** + * Parsed content with URL extracted for QR code display. + * Only relevant when showUrlAsQrCode is true. + */ + readonly parsedContent = computed(() => { + const data = this.modalRef.data; + if (!data.showUrlAsQrCode) { + return null; + } + return parseDialogContentForUrl(data.content); + }); + + /** + * Whether to show the QR code instead of the URL text. + */ + readonly shouldShowQrCode = computed(() => { + const parsed = this.parsedContent(); + return parsed !== null && parsed.url !== null; + }); + constructor( public modalRef: UiModalRef>, private _command: CommandService, diff --git a/apps/isa-app/src/ui/modal/dialog/dialog.helper.ts b/apps/isa-app/src/ui/modal/dialog/dialog.helper.ts new file mode 100644 index 000000000..b4ba98f3f --- /dev/null +++ b/apps/isa-app/src/ui/modal/dialog/dialog.helper.ts @@ -0,0 +1,48 @@ +import { ParsedDialogContent } from './dialog.model'; + +/** + * Regular expression to match URLs in text. + * Matches http:// and https:// URLs. + */ +const URL_REGEX = /https?:\/\/[^\s]+/i; + +/** + * Parses the dialog content and extracts the first URL. + * Splits the content into text before the URL, the URL itself, and text after. + * + * @param content - The dialog content string to parse + * @returns ParsedDialogContent with the split content + */ +export const parseDialogContentForUrl = ( + content: string | undefined, +): ParsedDialogContent => { + if (!content) { + return { textBefore: '', url: null, textAfter: '' }; + } + + const match = content.match(URL_REGEX); + + if (!match || match.index === undefined) { + return { textBefore: content, url: null, textAfter: '' }; + } + + const url = match[0]; + const urlIndex = match.index; + const textBefore = content.substring(0, urlIndex).trim(); + const textAfter = content.substring(urlIndex + url.length).trim(); + + return { textBefore, url, textAfter }; +}; + +/** + * Checks if the given content contains a URL. + * + * @param content - The content string to check + * @returns true if a URL is found, false otherwise + */ +export const contentHasUrl = (content: string | undefined): boolean => { + if (!content) { + return false; + } + return URL_REGEX.test(content); +}; diff --git a/apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts b/apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts new file mode 100644 index 000000000..a7317312e --- /dev/null +++ b/apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts @@ -0,0 +1,152 @@ +import { contentHasUrl, parseDialogContentForUrl } from './dialog.helper'; +import { ParsedDialogContent } from './dialog.model'; + +describe('parseDialogContentForUrl', () => { + it('should return empty result for undefined content', () => { + const result = parseDialogContentForUrl(undefined); + + expect(result).toEqual({ + textBefore: '', + url: null, + textAfter: '', + }); + }); + + it('should return empty result for empty string', () => { + const result = parseDialogContentForUrl(''); + + expect(result).toEqual({ + textBefore: '', + url: null, + textAfter: '', + }); + }); + + it('should return content as textBefore when no URL is found', () => { + const content = 'This is some text without a URL'; + + const result = parseDialogContentForUrl(content); + + expect(result).toEqual({ + textBefore: content, + url: null, + textAfter: '', + }); + }); + + it('should extract https URL from content', () => { + const content = 'Text before https://example.com text after'; + + const result = parseDialogContentForUrl(content); + + expect(result).toEqual({ + textBefore: 'Text before', + url: 'https://example.com', + textAfter: 'text after', + }); + }); + + it('should extract http URL from content', () => { + const content = 'Text before http://example.com text after'; + + const result = parseDialogContentForUrl(content); + + expect(result).toEqual({ + textBefore: 'Text before', + url: 'http://example.com', + textAfter: 'text after', + }); + }); + + it('should handle URL at the beginning of content', () => { + const content = 'https://example.com/path text after'; + + const result = parseDialogContentForUrl(content); + + expect(result).toEqual({ + textBefore: '', + url: 'https://example.com/path', + textAfter: 'text after', + }); + }); + + it('should handle URL at the end of content', () => { + const content = 'Text before https://example.com/path'; + + const result = parseDialogContentForUrl(content); + + expect(result).toEqual({ + textBefore: 'Text before', + url: 'https://example.com/path', + textAfter: '', + }); + }); + + it('should handle real-world content with newlines', () => { + const content = `Punkte: 80500 +Um alle Vorteile der Kundenkarte nutzen zu können, ist eine Verknüpfung zu einem Online-Konto notwendig. Kund:innen können sich über den QR-Code selbstständig anmelden oder die Kundenkarte dem bestehendem Konto hinzufügen. Bereits gesammelte Punkte werden übernommen. +https://h-k.me/QOHNTFVA`; + + const result = parseDialogContentForUrl(content); + + expect(result.url).toBe('https://h-k.me/QOHNTFVA'); + expect(result.textBefore).toContain('Punkte: 80500'); + expect(result.textBefore).toContain( + 'Bereits gesammelte Punkte werden übernommen.', + ); + expect(result.textAfter).toBe(''); + }); + + it('should extract only the first URL when multiple URLs are present', () => { + const content = 'First https://first.com then https://second.com'; + + const result = parseDialogContentForUrl(content); + + expect(result).toEqual({ + textBefore: 'First', + url: 'https://first.com', + textAfter: 'then https://second.com', + }); + }); + + it('should handle URLs with paths and query parameters', () => { + const content = + 'Visit https://example.com/path?query=value&foo=bar for more'; + + const result = parseDialogContentForUrl(content); + + expect(result.url).toBe('https://example.com/path?query=value&foo=bar'); + expect(result.textBefore).toBe('Visit'); + expect(result.textAfter).toBe('for more'); + }); +}); + +describe('contentHasUrl', () => { + it('should return false for undefined content', () => { + expect(contentHasUrl(undefined)).toBe(false); + }); + + it('should return false for empty string', () => { + expect(contentHasUrl('')).toBe(false); + }); + + it('should return false for content without URL', () => { + expect(contentHasUrl('This is text without a URL')).toBe(false); + }); + + it('should return true for content with https URL', () => { + expect(contentHasUrl('Check out https://example.com')).toBe(true); + }); + + it('should return true for content with http URL', () => { + expect(contentHasUrl('Check out http://example.com')).toBe(true); + }); + + it('should return true for real-world content', () => { + const content = `Punkte: 80500 +Um alle Vorteile der Kundenkarte nutzen zu können... +https://h-k.me/QOHNTFVA`; + + expect(contentHasUrl(content)).toBe(true); + }); +}); diff --git a/apps/isa-app/src/ui/modal/dialog/dialog.model.ts b/apps/isa-app/src/ui/modal/dialog/dialog.model.ts index 77005d38a..3a416abd2 100644 --- a/apps/isa-app/src/ui/modal/dialog/dialog.model.ts +++ b/apps/isa-app/src/ui/modal/dialog/dialog.model.ts @@ -1,4 +1,7 @@ -import { DialogSettings, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api'; +import { + DialogSettings, + KeyValueDTOOfStringAndString, +} from '@generated/swagger/crm-api'; export interface DialogModel { actions?: Array; @@ -14,4 +17,21 @@ export interface DialogModel { * default: true */ handleCommand?: boolean; + /** + * If true, URLs in the content will be displayed as QR codes. + * default: false + */ + showUrlAsQrCode?: boolean; +} + +/** + * Result of parsing content for URLs + */ +export interface ParsedDialogContent { + /** Text before the URL */ + textBefore: string; + /** The extracted URL (if any) */ + url: string | null; + /** Text after the URL */ + textAfter: string; } diff --git a/apps/isa-app/src/ui/modal/dialog/open-dialog.interceptor.ts b/apps/isa-app/src/ui/modal/dialog/open-dialog.interceptor.ts index 5ef09c091..2da2d7670 100644 --- a/apps/isa-app/src/ui/modal/dialog/open-dialog.interceptor.ts +++ b/apps/isa-app/src/ui/modal/dialog/open-dialog.interceptor.ts @@ -13,6 +13,7 @@ import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; import { DialogModel } from './dialog.model'; import { ToasterService } from '@shared/shell'; +import { contentHasUrl } from './dialog.helper'; @Injectable() export class OpenDialogInterceptor implements HttpInterceptor { @@ -21,7 +22,10 @@ export class OpenDialogInterceptor implements HttpInterceptor { private _toast: ToasterService, ) {} - intercept(req: HttpRequest, next: HttpHandler): Observable> { + intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { return next.handle(req).pipe( tap((response) => { if (response instanceof HttpResponse) { @@ -59,9 +63,17 @@ export class OpenDialogInterceptor implements HttpInterceptor { } openDialog(model: DialogModel) { + // Auto-detect URLs and enable QR code display if URL is found + // Can be overridden by explicitly setting showUrlAsQrCode in the model + const showUrlAsQrCode = + model.showUrlAsQrCode ?? contentHasUrl(model.content); + this._modal.open({ content: UiDialogModalComponent, - data: model, + data: { + ...model, + showUrlAsQrCode, + }, title: model.title, config: { canClose: (model.settings & 1) === 1, diff --git a/apps/isa-app/src/ui/modal/modal.module.ts b/apps/isa-app/src/ui/modal/modal.module.ts index 03061e45c..cdc69f0fa 100644 --- a/apps/isa-app/src/ui/modal/modal.module.ts +++ b/apps/isa-app/src/ui/modal/modal.module.ts @@ -2,7 +2,6 @@ import { OverlayModule } from '@angular/cdk/overlay'; import { CommonModule } from '@angular/common'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { UiModalComponent } from './modal.component'; -import { UiModalService } from './modal.service'; import { UiDebugModalComponent } from './debug-modal/debug-modal.component'; import { UiMessageModalComponent } from './message-modal.component'; import { UiIconModule } from '@ui/icon'; @@ -10,9 +9,10 @@ import { UiDialogModalComponent } from './dialog/dialog-modal.component'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { OpenDialogInterceptor } from './dialog/open-dialog.interceptor'; import { UiPromptModalComponent } from './prompt-modal'; +import { QRCodeComponent } from 'angularx-qrcode'; @NgModule({ - imports: [CommonModule, OverlayModule, UiIconModule], + imports: [CommonModule, OverlayModule, UiIconModule, QRCodeComponent], declarations: [ UiModalComponent, UiDebugModalComponent, diff --git a/package-lock.json b/package-lock.json index e59fa4e67..08d35ff35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@ngrx/store-devtools": "^20.0.0", "angular-oauth2-oidc": "^20.0.2", "angular-oauth2-oidc-jwks": "^20.0.0", + "angularx-qrcode": "^20.0.0", "date-fns": "^4.1.0", "jsbarcode": "^3.12.1", "lodash": "^4.17.21", @@ -15055,6 +15056,19 @@ "tslib": "^2.5.2" } }, + "node_modules/angularx-qrcode": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-20.0.0.tgz", + "integrity": "sha512-WZolRZztQsQxOXqodNSDicxPWNO79t/AT4wts+DxwYdtdXb1RELfZjtax9oGMQQ6mEZ6bwk5GqBGEDB3Y+cSqw==", + "license": "MIT", + "dependencies": { + "qrcode": "1.5.4", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^20.0.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -15098,7 +15112,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15108,7 +15121,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -15794,37 +15806,28 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bonjour-service": { @@ -16189,7 +16192,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -16630,7 +16632,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -16643,7 +16644,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -18307,6 +18307,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -18595,6 +18604,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -29306,9 +29321,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -30412,7 +30427,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -30673,7 +30687,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -30943,6 +30956,15 @@ "node": ">=8" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/portfinder": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", @@ -32100,6 +32122,156 @@ ], "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -32583,7 +32755,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -32599,6 +32770,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -33812,6 +33989,12 @@ "node": ">= 18" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -34599,7 +34782,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -37831,6 +38013,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -37876,7 +38064,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -37942,14 +38129,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -37959,7 +38144,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", diff --git a/package.json b/package.json index c59f5a2ba..1d08d4489 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@ngrx/store-devtools": "^20.0.0", "angular-oauth2-oidc": "^20.0.2", "angular-oauth2-oidc-jwks": "^20.0.0", + "angularx-qrcode": "^20.0.0", "date-fns": "^4.1.0", "jsbarcode": "^3.12.1", "lodash": "^4.17.21",