From 3cefaf85d9e33678332d4041121d9ec256f76cf3 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 21 Jul 2026 14:55:46 -0300 Subject: [PATCH] feat(qr-modal): implement QR modal component and integrate with tickets page --- src/app/core/services/modal.service.spec.ts | 92 +++++++------ src/app/core/services/modal.service.ts | 121 +++++++++--------- .../pages/tickets-page/tickets-page.html | 1 + .../pages/tickets-page/tickets-page.spec.ts | 39 ++++++ .../pages/tickets-page/tickets-page.ts | 11 ++ .../modal-shell/modal-shell.component.scss | 34 ++++- .../modal-shell/modal-shell.component.ts | 11 +- .../qr-modal/qr-modal.component.html | 16 +++ .../qr-modal/qr-modal.component.scss | 44 +++++++ .../components/qr-modal/qr-modal.component.ts | 15 +++ 10 files changed, 274 insertions(+), 110 deletions(-) create mode 100644 src/app/shared/components/qr-modal/qr-modal.component.html create mode 100644 src/app/shared/components/qr-modal/qr-modal.component.scss create mode 100644 src/app/shared/components/qr-modal/qr-modal.component.ts diff --git a/src/app/core/services/modal.service.spec.ts b/src/app/core/services/modal.service.spec.ts index 0f9a114..f258b47 100644 --- a/src/app/core/services/modal.service.spec.ts +++ b/src/app/core/services/modal.service.spec.ts @@ -1,37 +1,23 @@ import '@angular/compiler'; import { Component, inject } from '@angular/core'; import { TestBed, getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting -} from '@angular/platform-browser/testing'; -import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - vi -} from 'vitest'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { firstValueFrom } from 'rxjs'; import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component'; import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component'; +import { QrModalComponent } from '../../shared/components/qr-modal/qr-modal.component'; import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component'; -import { - MODAL_DATA, - ModalRef, - ModalService -} from './modal.service'; +import { MODAL_DATA, ModalRef, ModalService } from './modal.service'; @Component({ - template: '' + template: '', }) class FirstTestModalComponent {} @Component({ - template: '' + template: '', }) class SecondTestModalComponent {} @@ -40,10 +26,7 @@ describe('ModalService', () => { beforeAll(() => { try { - getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() - ); + getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); } catch { // Test environment may already be initialized by another setup entrypoint. } @@ -55,7 +38,7 @@ describe('ModalService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ModalService] + providers: [ModalService], }); service = TestBed.inject(ModalService); @@ -66,7 +49,7 @@ describe('ModalService', () => { title: 'Confirmar compra', data: { productId: 10 }, size: 'lg', - closeOnBackdrop: false + closeOnBackdrop: false, }); const activeModal = service.activeModal(); @@ -80,7 +63,7 @@ describe('ModalService', () => { size: 'lg', closeOnBackdrop: false, closeOnEscape: true, - showCloseButton: true + showCloseButton: true, }); }); @@ -115,7 +98,7 @@ describe('ModalService', () => { firstRef.afterClosed$.subscribe(firstClosedSpy); const secondRef = service.open(SecondTestModalComponent, { - title: 'Segundo modal' + title: 'Segundo modal', }); expect(firstClosedSpy).toHaveBeenCalledWith(undefined); @@ -126,7 +109,7 @@ describe('ModalService', () => { it('injects MODAL_DATA and ModalRef into opened components through the host injector contract', () => { const ref = service.open(DataTestModalComponent, { - data: { amount: 3 } + data: { amount: 3 }, }); const activeModal = service.activeModal(); @@ -138,7 +121,7 @@ describe('ModalService', () => { it('opens the standard confirm modal with default labels', () => { const result$ = service.openConfirm({ title: 'Confirmar compra', - content: 'Esto confirmara la compra actual.' + content: 'Esto confirmara la compra actual.', }); const activeModal = service.activeModal(); @@ -150,19 +133,19 @@ describe('ModalService', () => { data: { content: 'Esto confirmara la compra actual.', confirmLabel: 'Confirmar', - cancelLabel: 'Cancelar' + cancelLabel: 'Cancelar', }, size: 'md', closeOnBackdrop: true, closeOnEscape: true, - showCloseButton: true + showCloseButton: true, }); }); it('maps the confirm modal close result to true', async () => { const result$ = service.openConfirm({ title: 'Confirmar compra', - content: 'Esto confirmara la compra actual.' + content: 'Esto confirmara la compra actual.', }); const activeModal = service.activeModal(); const resultPromise = firstValueFrom(result$); @@ -175,7 +158,7 @@ describe('ModalService', () => { it('maps dismissing a confirm modal to false', async () => { const result$ = service.openConfirmDelete({ title: 'Eliminar producto', - content: 'Se eliminara el producto.' + content: 'Se eliminara el producto.', }); const activeModal = service.activeModal(); const resultPromise = firstValueFrom(result$); @@ -194,7 +177,7 @@ describe('ModalService', () => { size: 'lg', closeOnBackdrop: false, closeOnEscape: false, - showCloseButton: false + showCloseButton: false, }); const activeModal = service.activeModal(); @@ -205,19 +188,19 @@ describe('ModalService', () => { data: { content: 'Se eliminara el producto.', confirmLabel: 'Eliminar', - cancelLabel: 'Conservar' + cancelLabel: 'Conservar', }, size: 'lg', closeOnBackdrop: false, closeOnEscape: false, - showCloseButton: false + showCloseButton: false, }); }); it('opens the simple modal with default button label', () => { service.openSimple({ title: 'Aviso', - content: 'Este es un aviso simple.' + content: 'Este es un aviso simple.', }); const activeModal = service.activeModal(); @@ -227,19 +210,19 @@ describe('ModalService', () => { title: 'Aviso', data: { content: 'Este es un aviso simple.', - buttonLabel: 'Entendido' + buttonLabel: 'Entendido', }, size: 'md', closeOnBackdrop: true, closeOnEscape: true, - showCloseButton: true + showCloseButton: true, }); }); it('maps the simple modal close result to undefined', async () => { const result$ = service.openSimple({ title: 'Aviso', - content: 'Este es un aviso simple.' + content: 'Este es un aviso simple.', }); const activeModal = service.activeModal(); const resultPromise = firstValueFrom(result$); @@ -248,10 +231,35 @@ describe('ModalService', () => { await expect(resultPromise).resolves.toBeUndefined(); }); + + it('opens the QR modal with the ticket information and compact size', () => { + service.openQr({ + title: 'Entrada general', + id: 42, + date: '09 de octubre', + ticket: 'ticket-payload', + }); + + const activeModal = service.activeModal(); + + expect(activeModal?.component).toBe(QrModalComponent); + expect(activeModal?.config).toEqual({ + title: 'Entrada general', + data: { + id: 42, + date: '09 de octubre', + ticket: 'ticket-payload', + }, + size: 'qr', + closeOnBackdrop: true, + closeOnEscape: true, + showCloseButton: true, + }); + }); }); @Component({ - template: '' + template: '', }) class DataTestModalComponent { readonly data = inject(MODAL_DATA); diff --git a/src/app/core/services/modal.service.ts b/src/app/core/services/modal.service.ts index 98fbe87..4abdcb1 100644 --- a/src/app/core/services/modal.service.ts +++ b/src/app/core/services/modal.service.ts @@ -2,14 +2,11 @@ import { InjectionToken, Injectable, Type, signal } from '@angular/core'; import { Observable, Subject, map } from 'rxjs'; import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component'; import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component'; +import { QrModalComponent } from '../../shared/components/qr-modal/qr-modal.component'; import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component'; -export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'; -export type ModalDismissReason = - | 'backdrop' - | 'escape' - | 'programmatic' - | 'replaced'; +export type ModalSize = 'qr' | 'sm' | 'md' | 'lg' | 'xl' | 'full'; +export type ModalDismissReason = 'backdrop' | 'escape' | 'programmatic' | 'replaced'; export interface ModalConfig { title?: string; @@ -20,11 +17,10 @@ export interface ModalConfig { showCloseButton?: boolean; } -export interface NormalizedModalConfig - extends Omit< - ModalConfig, - 'size' | 'closeOnBackdrop' | 'closeOnEscape' | 'showCloseButton' - > { +export interface NormalizedModalConfig extends Omit< + ModalConfig, + 'size' | 'closeOnBackdrop' | 'closeOnEscape' | 'showCloseButton' +> { size: ModalSize; closeOnBackdrop: boolean; closeOnEscape: boolean; @@ -37,8 +33,7 @@ export interface ConfirmModalData { cancelLabel: string; } -export interface ConfirmModalConfig - extends Omit, 'data'> { +export interface ConfirmModalConfig extends Omit, 'data'> { content: string; confirmLabel?: string; cancelLabel?: string; @@ -49,12 +44,24 @@ export interface SimpleModalData { buttonLabel: string; } -export interface SimpleModalConfig - extends Omit, 'data'> { +export interface SimpleModalConfig extends Omit, 'data'> { content: string; buttonLabel?: string; } +export interface QrModalData { + id: string | number; + date: string; + ticket: string; +} + +export interface QrModalConfig extends Omit, 'data' | 'title'> { + title: string; + id: string | number; + date: string; + ticket: string; +} + export interface ActiveModalState { component: Type; config: NormalizedModalConfig; @@ -70,25 +77,24 @@ const DEFAULT_MODAL_CONFIG: Pick< size: 'md', closeOnBackdrop: true, closeOnEscape: true, - showCloseButton: true + showCloseButton: true, }; const DEFAULT_CONFIRM_MODAL_LABELS = { confirmLabel: 'Confirmar', - cancelLabel: 'Cancelar' + cancelLabel: 'Cancelar', } satisfies Pick; export class ModalRef { private readonly afterClosedSubject = new Subject(); private closed = false; - readonly afterClosed$: Observable = - this.afterClosedSubject.asObservable(); + readonly afterClosed$: Observable = this.afterClosedSubject.asObservable(); readonly dismissReason = signal(null); constructor( private readonly closeHandler: (result?: TResult) => void, - private readonly dismissHandler: (reason: ModalDismissReason) => void + private readonly dismissHandler: (reason: ModalDismissReason) => void, ) {} close(result?: TResult): void { @@ -120,7 +126,7 @@ export class ModalRef { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ModalService { private readonly activeModalState = signal(null); @@ -128,35 +134,31 @@ export class ModalService { open( component: Type, - config: ModalConfig = {} + config: ModalConfig = {}, ): ModalRef { this.activeModalState()?.ref.dismiss('replaced'); let ref!: ModalRef; ref = new ModalRef( (result) => this.close(ref, result), - (reason) => this.dismiss(ref, reason) + (reason) => this.dismiss(ref, reason), ); this.activeModalState.set({ component, config: this.normalizeConfig(config), - ref: ref as ModalRef + ref: ref as ModalRef, }); return ref; } openConfirm(config: ConfirmModalConfig): Observable { - return this.openConfirmRef(config).afterClosed$.pipe( - map((result) => result === true) - ); + return this.openConfirmRef(config).afterClosed$.pipe(map((result) => result === true)); } openConfirmDelete(config: ConfirmModalConfig): Observable { - return this.openConfirmDeleteRef(config).afterClosed$.pipe( - map((result) => result === true) - ); + return this.openConfirmDeleteRef(config).afterClosed$.pipe(map((result) => result === true)); } openConfirmRef(config: ConfirmModalConfig): ModalRef { @@ -164,23 +166,30 @@ export class ModalService { } openConfirmDeleteRef(config: ConfirmModalConfig): ModalRef { - return this.open( - ConfirmDeleteModalComponent, - this.buildConfirmModalConfig(config) - ); + return this.open(ConfirmDeleteModalComponent, this.buildConfirmModalConfig(config)); } openSimple(config: SimpleModalConfig): Observable { - return this.openSimpleRef(config).afterClosed$.pipe( - map(() => undefined) - ); + return this.openSimpleRef(config).afterClosed$.pipe(map(() => undefined)); } openSimpleRef(config: SimpleModalConfig): ModalRef { - return this.open( - SimpleModalComponent, - this.buildSimpleModalConfig(config) - ); + return this.open(SimpleModalComponent, this.buildSimpleModalConfig(config)); + } + + openQr(config: QrModalConfig): Observable { + return this.openQrRef(config).afterClosed$.pipe(map(() => undefined)); + } + + openQrRef(config: QrModalConfig): ModalRef { + const { title, id, date, ticket, size = 'qr', ...modalConfig } = config; + + return this.open(QrModalComponent, { + ...modalConfig, + title, + size, + data: { id, date, ticket }, + }); } private close(ref: ModalRef, result?: TResult): void { @@ -194,7 +203,7 @@ export class ModalService { private dismiss( ref: ModalRef, - reason: ModalDismissReason = 'programmatic' + reason: ModalDismissReason = 'programmatic', ): void { if (this.activeModalState()?.ref !== ref) { return; @@ -204,18 +213,14 @@ export class ModalService { this.activeModalState.set(null); } - private normalizeConfig( - config: ModalConfig - ): NormalizedModalConfig { + private normalizeConfig(config: ModalConfig): NormalizedModalConfig { return { ...DEFAULT_MODAL_CONFIG, - ...config + ...config, }; } - private buildConfirmModalConfig( - config: ConfirmModalConfig - ): ModalConfig { + private buildConfirmModalConfig(config: ConfirmModalConfig): ModalConfig { const { content, confirmLabel = DEFAULT_CONFIRM_MODAL_LABELS.confirmLabel, @@ -228,26 +233,20 @@ export class ModalService { data: { content, confirmLabel, - cancelLabel - } + cancelLabel, + }, }; } - private buildSimpleModalConfig( - config: SimpleModalConfig - ): ModalConfig { - const { - content, - buttonLabel = 'Entendido', - ...modalConfig - } = config; + private buildSimpleModalConfig(config: SimpleModalConfig): ModalConfig { + const { content, buttonLabel = 'Entendido', ...modalConfig } = config; return { ...modalConfig, data: { content, - buttonLabel - } + buttonLabel, + }, }; } } diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html index a61587f..1a573cf 100644 --- a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html +++ b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html @@ -37,6 +37,7 @@ [date]="formatDate(ticket)" [selected]="isSelected(ticket.id)" (selectedChange)="toggleTicket(ticket.id, $event)" + (viewQr)="viewQr(ticket)" /> } diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.spec.ts b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.spec.ts index 751147b..26de833 100644 --- a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.spec.ts +++ b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.spec.ts @@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { describe, expect, it, vi } from 'vitest'; import { ToastService } from '../../../../../../core/services/toast.service'; +import { ModalService } from '../../../../../../core/services/modal.service'; import { TicketComponent } from './components/ticket/ticket.component'; import { TicketResponse, TicketService } from './ticket.service'; import { TicketsPage } from './tickets-page'; @@ -76,4 +77,42 @@ describe('TicketsPage', () => { expect(element.querySelector('[aria-label="Compartir ticket"]')).toBeNull(); }); }); + + it('opens the QR modal with the selected ticket data', async () => { + const modalService = { openQr: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [TicketsPage], + providers: [ + { provide: ToastService, useValue: { danger: vi.fn() } }, + { provide: ModalService, useValue: modalService }, + ], + }) + .overrideComponent(TicketsPage, { + set: { + providers: [ + { + provide: TicketService, + useValue: { getTickets: () => Promise.resolve([ticket({})]) }, + }, + ], + }, + }) + .compileComponents(); + + const fixture = TestBed.createComponent(TicketsPage); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const page = fixture.nativeElement as HTMLElement; + page.querySelector('app-ticket app-button button')?.click(); + + expect(modalService.openQr).toHaveBeenCalledWith({ + title: 'Entrada general', + id: 1, + date: '09-octubre', + ticket: 'ticket-1', + }); + }); }); diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts index be00505..257d355 100644 --- a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts +++ b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts @@ -1,5 +1,6 @@ import { Component, computed, inject, OnInit, signal } from '@angular/core'; +import { ModalService } from '../../../../../../core/services/modal.service'; import { ToastService } from '../../../../../../core/services/toast.service'; import { IconButtonComponent } from '../../../../../../shared/components/icon-button/icon-button.component'; import { TicketComponent } from './components/ticket/ticket.component'; @@ -15,6 +16,7 @@ import { TicketResponse, TicketService } from './ticket.service'; export class TicketsPage implements OnInit { private readonly ticketService = inject(TicketService); private readonly toastService = inject(ToastService); + private readonly modalService = inject(ModalService); protected readonly tickets = signal([]); protected readonly activeTickets = computed(() => @@ -75,4 +77,13 @@ export class TicketsPage implements OnInit { timeZone: 'UTC', }).format(date); } + + protected viewQr(ticket: TicketResponse): void { + this.modalService.openQr({ + title: ticket.name, + id: ticket.id, + date: this.formatDate(ticket) ?? '', + ticket: ticket.ticket, + }); + } } diff --git a/src/app/shared/components/modal-shell/modal-shell.component.scss b/src/app/shared/components/modal-shell/modal-shell.component.scss index 1da346e..d5f80b8 100644 --- a/src/app/shared/components/modal-shell/modal-shell.component.scss +++ b/src/app/shared/components/modal-shell/modal-shell.component.scss @@ -19,6 +19,36 @@ margin: 0; } +.modal-shell__dialog--qr { + width: min(100%, 15rem); +} + +.modal-shell__dialog--qr .modal-shell__content { + min-height: auto; + border-radius: 0.35rem; + background: #ffffff; +} + +.modal-shell__dialog--qr .modal-shell__header { + padding: 1.7rem 1rem 0.35rem; +} + +.modal-shell__dialog--qr .modal-shell__title { + color: var(--bs-success, #198754); + font-size: 0.8rem; + font-weight: 800; +} + +.modal-shell__dialog--qr .modal-shell__header .btn-close { + top: 0.85rem; + right: 0.85rem; + font-size: 0.7rem; +} + +.modal-shell__dialog--qr .modal-shell__body { + padding: 0 1rem 1rem; +} + .modal-shell__dialog.modal-sm { width: min(100%, 24rem); } @@ -40,8 +70,7 @@ min-height: 180px; max-height: calc(100dvh - 2rem); border-radius: 1.25rem; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 248, 248, 0.98)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 248, 248, 0.98)); } .modal-shell__dialog--full .modal-shell__content { @@ -88,6 +117,7 @@ } .modal-shell__dialog, + .modal-shell__dialog--qr, .modal-shell__dialog.modal-sm, .modal-shell__dialog.modal-lg, .modal-shell__dialog.modal-xl, diff --git a/src/app/shared/components/modal-shell/modal-shell.component.ts b/src/app/shared/components/modal-shell/modal-shell.component.ts index 58d0458..7062343 100644 --- a/src/app/shared/components/modal-shell/modal-shell.component.ts +++ b/src/app/shared/components/modal-shell/modal-shell.component.ts @@ -6,7 +6,7 @@ import { computed, input, output, - viewChild + viewChild, } from '@angular/core'; import { ModalSize } from '../../../core/services/modal.service'; @@ -16,7 +16,7 @@ import { ModalSize } from '../../../core/services/modal.service'; imports: [NgClass], templateUrl: './modal-shell.component.html', styleUrl: './modal-shell.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ModalShellComponent { private readonly panel = viewChild.required>('panel'); @@ -32,10 +32,11 @@ export class ModalShellComponent { const size = this.size(); return { + 'modal-shell__dialog--qr': size === 'qr', 'modal-sm': size === 'sm', 'modal-lg': size === 'lg', 'modal-xl': size === 'xl', - 'modal-shell__dialog--full': size === 'full' + 'modal-shell__dialog--full': size === 'full', }; }); @@ -55,8 +56,8 @@ export class ModalShellComponent { 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', - '[tabindex]:not([tabindex="-1"])' - ].join(', ') + '[tabindex]:not([tabindex="-1"])', + ].join(', '), ); (focusTarget ?? panel).focus(); diff --git a/src/app/shared/components/qr-modal/qr-modal.component.html b/src/app/shared/components/qr-modal/qr-modal.component.html new file mode 100644 index 0000000..ff5300f --- /dev/null +++ b/src/app/shared/components/qr-modal/qr-modal.component.html @@ -0,0 +1,16 @@ +
+
+

ID: {{ data.id }}

+ @if (data.date) { + + } +
+ + +
diff --git a/src/app/shared/components/qr-modal/qr-modal.component.scss b/src/app/shared/components/qr-modal/qr-modal.component.scss new file mode 100644 index 0000000..f8ede92 --- /dev/null +++ b/src/app/shared/components/qr-modal/qr-modal.component.scss @@ -0,0 +1,44 @@ +.qr-modal { + display: grid; + justify-items: center; + gap: 0.65rem; + width: 100%; + text-align: center; +} + +.qr-modal__header { + display: grid; + justify-items: center; + gap: 0.25rem; +} + +.qr-modal__id { + margin: 0; +} + +.qr-modal__id { + color: #4f4f4f; + font-size: 0.68rem; + line-height: 1.2; +} + +.qr-modal__date { + margin-top: 0.35rem; + color: #8a8a8a; + font-size: 0.62rem; + line-height: 1.2; +} + +.qr-modal__code { + display: block; + width: 100%; + max-width: 11rem; + overflow: hidden; + background: #ffffff; +} + +:host ::ng-deep .qr-modal__code canvas { + display: block; + width: 100% !important; + height: auto !important; +} diff --git a/src/app/shared/components/qr-modal/qr-modal.component.ts b/src/app/shared/components/qr-modal/qr-modal.component.ts new file mode 100644 index 0000000..b409cd4 --- /dev/null +++ b/src/app/shared/components/qr-modal/qr-modal.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { MODAL_DATA, QrModalData } from '../../../core/services/modal.service'; +import { QRCodeComponent } from '../qrcode/qrcode.component'; + +@Component({ + selector: 'app-qr-modal', + imports: [QRCodeComponent], + templateUrl: './qr-modal.component.html', + styleUrl: './qr-modal.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class QrModalComponent { + protected readonly data = inject(MODAL_DATA); +}