UNPKG

ng-smart-on-fhir

Version:

[![SRDC](https://www.srdc.com.tr/wp-content/uploads/2014/12/srdc-wp.png)](https://srdc.com.tr/en)

1,189 lines 65.8 kB
import * as i0 from '@angular/core';
import { Inject, Injectable, ViewChild, Component, Pipe, NgModule, inject } from '@angular/core';
import * as FHIR from 'fhirclient';
import { Subject, firstValueFrom } from 'rxjs';
import * as i1 from '@angular/router';
import { RouterModule, Router } from '@angular/router';
import * as i5 from '@angular/common';
import { NgStyle, KeyValuePipe, AsyncPipe, CommonModule } from '@angular/common';
import * as i5$1 from '@angular/forms';
import { FormsModule } from '@angular/forms';
import jsQR from 'jsqr';
import * as jose from 'jose';
import * as pako from 'pako';
import * as i1$1 from '@angular/common/http';

/**
 * SMART Authentication Service
 * Handles authentication/authorization using the fhirclient library (or offline via SHC/SHL), and stores the state
 */
class SmartAuthService {
    constructor(config = {}) {
        this.config = config;
        // selected login method
        this.method = 'offline';
        // selected client
        this.selectedClient = undefined;
        // auth enabled (not public)
        this.enabled = true;
        this.SAVED_SESSION_KEY = 'SMART_APPS_SESSION';
        this.client$ = new Subject();
        this.clientPromise = firstValueFrom(this.client$);
        this.LAUNCHED_URL = 'SMART_APPS_LAUNCHED_URL';
    }
    isLoggedIn() {
        return !!this.patient || (this.selectedClient?.scope?.includes('launch/patient') ? false : !!this.user);
    }
    /**
     * Login
     * @param config - selected client's config
     * @param patient - selected patient, if applicable
     */
    login(config, patient) {
        this.method = 'login';
        this.selectedClient = config;
        this.enabled = !config.isPublic;
        this.patient = patient;
        this.saveSession();
        if (!config.isPublic) {
            FHIR.oauth2.authorize({
                iss: config.iss,
                redirectUri: config.redirectUri,
                clientId: config.clientId,
                scope: config.scope,
                noRedirect: true
            }).then(redirectUrl => {
                const [url, params] = redirectUrl.split('?');
                let queryParams = params.split('&');
                if (config.aud) {
                    queryParams = queryParams.filter(param => !param.startsWith('aud='));
                    queryParams.push('aud=' + config.aud);
                }
                if (config.promptLogin) {
                    queryParams.push('prompt=login');
                }
                window.location.href = [url, queryParams.join('&')].join('?');
            }, console.error);
        }
        else if (patient) {
            this.client$.next(FHIR.client(config.iss));
        }
    }
    // get public client without authentication
    publicClient(iss) {
        return FHIR.client(iss);
    }
    // launch with selected issuer
    launch(iss, launch, clientConfig) {
        const clientId = this.config.clientId || (this.config.clientIds && this.config.clientIds[iss]);
        this.method = 'launch';
        this.selectedClient = clientConfig;
        this.enabled = true;
        this.saveSession({
            [this.LAUNCHED_URL]: window.location.href
        });
        FHIR.oauth2.authorize({
            clientId, iss, launch,
            redirectUri: this.config.redirectUrl
        });
    }
    // start auth flow
    async start() {
        const sessionContext = this.restoreSession();
        if (sessionContext?.method === 'offline') {
            this.offline(sessionContext.patient, sessionContext.shc || []);
        }
        else if (sessionContext?.selectedClient?.isPublic) {
            this.client$.next(this.publicClient((sessionContext?.selectedClient).iss));
        }
        else {
            await FHIR.oauth2.ready().then(async (client) => {
                this.checkPatientInToken(client);
                if (this.config.authStorage === 'localStorage') {
                    const smartKey = sessionStorage.getItem('SMART_KEY');
                    if (smartKey) {
                        localStorage.setItem('SMART_KEY', smartKey);
                        localStorage.setItem(smartKey, sessionStorage.getItem(smartKey));
                    }
                }
                this.user = (await client.user.read().catch(() => undefined));
                this.patient = await client.patient.read().catch(() => undefined);
                this.client$.next(client);
            });
        }
        return sessionContext;
    }
    // offline mode with SHCs
    offline(patient, shc) {
        this.patient = patient;
        this.user = undefined;
        this.method = 'offline';
        this.selectedClient = undefined;
        this.client$.next(undefined);
        this.saveSession({ shc });
    }
    // store session in the session storage
    saveSession(params) {
        sessionStorage.setItem(this.SAVED_SESSION_KEY, JSON.stringify({
            patient: this.patient,
            user: this.user,
            method: this.method,
            selectedClient: this.selectedClient,
            enabled: this.enabled,
            ...(params || {})
        }));
        if (this.config.authStorage === 'localStorage') {
            localStorage.setItem(this.SAVED_SESSION_KEY, sessionStorage.getItem(this.SAVED_SESSION_KEY));
        }
    }
    // restore session from the local storage
    restoreSession() {
        if (this.config.authStorage === 'localStorage' && !sessionStorage.getItem(this.SAVED_SESSION_KEY)
            && localStorage.getItem(this.SAVED_SESSION_KEY)) {
            sessionStorage.setItem(this.SAVED_SESSION_KEY, localStorage.getItem(this.SAVED_SESSION_KEY));
            if (localStorage.getItem('SMART_KEY')) {
                const smartKey = localStorage.getItem('SMART_KEY');
                sessionStorage.setItem('SMART_KEY', smartKey);
                sessionStorage.setItem(smartKey, localStorage.getItem(smartKey));
            }
        }
        const saved = sessionStorage.getItem(this.SAVED_SESSION_KEY);
        if (saved) {
            const { patient, user, method, selectedClient, enabled, ...rest } = JSON.parse(saved);
            this.patient = patient;
            this.user = user;
            this.method = method;
            this.selectedClient = selectedClient;
            this.enabled = enabled;
            return { patient, user, method, selectedClient, enabled, ...rest };
        }
        return null;
    }
    // if the patient is not received properly from the response, but exists in the token, add it to the response
    checkPatientInToken(client) {
        const sessionId = sessionStorage['SMART_KEY'] && JSON.parse(sessionStorage['SMART_KEY']);
        if (sessionId && sessionStorage[sessionId]) {
            const session = JSON.parse(sessionStorage[sessionId]);
            const token = session.tokenResponse?.access_token;
            const id_token = session.tokenResponse?.id_token;
            const parsed = JSON.parse(atob(token.split('.')[1]));
            let changed = false;
            if (!client.user?.id && id_token) {
                const parts = id_token.split('.');
                const parsedIdToken = JSON.parse(atob(parts[1]));
                parsedIdToken.fhirUser = 'Practitioner/' + parsedIdToken.sub;
                session.tokenResponse.id_token = [parts[0], btoa(JSON.stringify(parsedIdToken)), parts[2]].join('.');
                changed = true;
            }
            if (!client.getPatientId() && parsed.patient) {
                session.tokenResponse.patient = parsed.patient;
                changed = true;
            }
            if (changed) {
                sessionStorage[sessionId] = JSON.stringify(session);
                window.location.reload();
            }
        }
    }
    getClient() {
        return this.clientPromise;
    }
    getClientConfig() {
        return this.selectedClient;
    }
    logout() {
        this.clearSession();
        this.getClient().then(client => {
            const loginClient = this.config?.loginClients?.find(lc => lc.iss === client?.state.serverUrl);
            if (loginClient?.logoutUri) {
                window.location.href = loginClient.logoutUri;
            }
            else {
                window.location.href = window.location.href.split('#')[0].replace(/shc$/, 'login');
            }
        }, err => window.location.reload());
    }
    clearSession() {
        sessionStorage.clear();
        if (this.config.authStorage === 'localStorage') {
            localStorage.clear();
        }
        this.method = 'offline';
        this.patient = this.user = undefined;
        this.selectedClient = undefined;
        this.enabled = true;
    }
    async getPatient() {
        if (this.patient)
            return this.patient;
        await this.getClient();
        return this.patient;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartAuthService, deps: [{ token: 'sofConfig' }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartAuthService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartAuthService, decorators: [{
            type: Injectable
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: ['sofConfig']
                }] }] });

/**
 * Authenticated FHIR client
 */
class SmartOnFhirService {
    get importedResources() {
        return this._importedResources;
    }
    constructor(config = {}, auth) {
        this.config = config;
        this.auth = auth;
        // imported local resources from SHCs
        this.importedSHCards = [];
        this._importedResources = {};
    }
    getClient() {
        return this.auth.getClient();
    }
    async ready(callback) {
        const client = await this.getClient();
        return client ? callback(client) : Promise.reject();
    }
    getPatient() {
        return this.auth.getPatient();
    }
    // FHIR Search
    search(resourceType, ...params) {
        return this.ready(client => client.request({
            url: this.constructQueryURL(resourceType, params)
        }));
    }
    // FHIR Create (POST/PUT)
    create(resource, id) {
        return this.ready(client => {
            if (id) {
                const _resource = { ...resource, id: id };
                return client.update(resource);
            }
            else {
                const _resource = { ...resource };
                delete _resource.id;
                return client.create(_resource);
            }
        });
    }
    // FHIR DELETE
    delete(resourceType, id, params) {
        if (!resourceType)
            return Promise.reject('Resource type should be provided.');
        return this.ready(client => {
            if (id) {
                return client.delete(resourceType + '/' + id);
            }
            else {
                return client.delete(this.constructQueryURL(resourceType, params));
            }
        });
    }
    // FHIR Operation
    operation(options) {
        const opUrl = (options.resourceType ? options.resourceType + (options.resourceId ? '/' + options.resourceId : '') + '/' : '')
            + '$' + options.operationName;
        return this.ready(client => client.request({
            method: 'POST',
            url: opUrl + (options.queryParams?.length ? this.constructQueryURL('', options.queryParams) : ''),
            body: JSON.stringify(options.params || {})
        }));
    }
    transaction(bundle, method) {
        let transactionBundle;
        if (method && Array.isArray(bundle)) {
            transactionBundle = {
                resourceType: 'Bundle',
                type: 'transaction',
                entry: bundle.map(resource => {
                    return {
                        resource,
                        request: {
                            method: method,
                            url: (method === 'PUT' || method === 'PATCH') && resource.id ? resource.resourceType + '/' + resource.id : resource.resourceType
                        }
                    };
                })
            };
        }
        else {
            transactionBundle = bundle;
        }
        return this.ready(client => client.request({
            method: 'POST',
            url: '/',
            body: JSON.stringify(transactionBundle)
        }));
    }
    // FHIR Request
    query(url) {
        if (url.length >= 2048) {
            return this.ready(client => client.request({
                method: 'POST',
                url: '',
                body: JSON.stringify({
                    resourceType: 'Bundle',
                    type: 'batch',
                    entry: [{
                            request: {
                                url,
                                method: 'GET'
                            }
                        }]
                })
            })).then(bundle => bundle.entry?.at(0)?.resource);
        }
        return this.ready(client => client.request({
            url: url + (url.includes('Observation') ? '&_count=999' : '')
        }));
    }
    logout() {
        this.auth.logout();
    }
    // Construct FHIR API url from the given query
    constructQueryURL(resourceType, params) {
        return resourceType + '?' + (params?.map(_params => Object.keys(_params).map(key => key + '=' + _params[key]).join('&')).filter(_ => _).join('&') || '');
    }
    // Import data from SHC
    importSHCData(param) {
        const { bundle, shc } = param;
        this.importedSHCards.push(param);
        this._importedResources = this.importedSHCards.map(shc => shc.bundle.entry?.map(entry => entry.resource) || []).flat()
            .reduce((o, r) => {
            if (!r) {
                return;
            }
            if (!o[r.resourceType]) {
                o[r.resourceType] = [];
            }
            o[r.resourceType].push(r);
            return o;
        }, {});
        const patient = bundle.entry?.find(entry => entry.resource?.resourceType === 'Patient')?.resource;
        this.auth.offline(patient, shc);
    }
    // Return all imported resources from SHC
    getAllImportedResources() {
        return Object.values(this.importedResources).flat();
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirService, deps: [{ token: 'sofConfig' }, { token: SmartAuthService }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirService, decorators: [{
            type: Injectable
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: ['sofConfig']
                }] }, { type: SmartAuthService }] });

/**
 * QR reader & parser component
 */
class QrReaderComponent {
    constructor(router) {
        this.router = router;
        this.scanning = false;
        this.qrResult = null;
    }
    ngOnInit() {
        window['jsqr'] = jsQR;
        this.startCamera();
    }
    async startCamera() {
        try {
            this.videoStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
            const video = this.videoElement.nativeElement;
            video.srcObject = this.videoStream;
            video.onloadedmetadata = () => {
                video.play();
                this.scanning = true;
                this.scanQRCode();
            };
        }
        catch (error) {
            console.error('Error accessing camera:', error);
        }
    }
    scanQRCode() {
        if (!this.scanning)
            return;
        const video = this.videoElement.nativeElement;
        const canvas = this.canvasElement.nativeElement;
        const ctx = canvas.getContext('2d');
        if (!ctx)
            return;
        const scan = () => {
            if (!this.scanning)
                return;
            // Ensure video dimensions are available
            if (video.videoWidth === 0 || video.videoHeight === 0) {
                requestAnimationFrame(scan);
                return;
            }
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const code = jsQR(imageData.data, imageData.width, imageData.height);
            if (code) {
                this.checkCode(code);
            }
            else {
                requestAnimationFrame(scan);
            }
        };
        scan();
    }
    scanFile(event) {
        const input = event.target;
        if (input.files && input.files[0]) {
            const file = input.files[0];
            const reader = new FileReader();
            reader.onload = (e) => {
                const img = new Image();
                img.src = e.target?.result;
                img.onload = () => {
                    const canvas = this.fileCanvasElement.nativeElement;
                    const ctx = canvas.getContext('2d');
                    if (ctx) {
                        canvas.width = img.width;
                        canvas.height = img.height;
                        ctx.drawImage(img, 0, 0, img.width, img.height);
                        const imageData = ctx.getImageData(0, 0, img.width, img.height);
                        const code = jsQR(imageData.data, imageData.width, imageData.height);
                        if (code) {
                            this.checkCode(code);
                        }
                        else {
                            this.error = 'The file does not contain a valid QR.';
                        }
                    }
                };
            };
            reader.readAsDataURL(file);
        }
    }
    stopCamera() {
        if (this.videoStream) {
            this.videoStream.getTracks().forEach(track => track.stop());
        }
        this.scanning = false;
    }
    ngOnDestroy() {
        this.stopCamera();
    }
    checkCode(code) {
        this.qrResult = code.data;
        if (!(this.qrResult.startsWith('shc:/') || this.qrResult.startsWith('shlink:/'))) {
            this.error = 'QR doesn\' contain a SMART Health Card or Link';
        }
        else {
            this.router.navigate([!this.qrResult.startsWith('shc:/') ? '/shc' : '/shl'], { fragment: this.qrResult });
            this.stopCamera();
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: QrReaderComponent, deps: [{ token: i1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: QrReaderComponent, selector: "sof-qr-reader", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["video"], descendants: true, static: true }, { propertyName: "canvasElement", first: true, predicate: ["canvas"], descendants: true, static: true }, { propertyName: "fileCanvasElement", first: true, predicate: ["fileCanvas"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"scanner-container\">\r\n  <h2>QR Scanner</h2>\r\n\r\n  <!-- Live Camera Scanner -->\r\n  <video #video class=\"qr-video\"></video>\r\n  <canvas #canvas class=\"qr-canvas\"></canvas>\r\n\r\n  <hr>\r\n\r\n  <!-- File Upload Scanner -->\r\n  <input type=\"file\" accept=\"image/*\" (change)=\"scanFile($event)\">\r\n  <canvas #fileCanvas class=\"qr-file-canvas\"></canvas>\r\n\r\n  @if (qrResult) {\r\n    <p>Scanned QR Code: {{ qrResult }}</p>\r\n  }\r\n</div>\r\n", styles: [".scanner-container{text-align:center;width:100%;max-width:500px;margin:auto}.qr-video{width:100%;height:auto;border:2px solid #000}.qr-canvas,.qr-file-canvas{display:none}input[type=file]{margin:10px 0}\n"] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: QrReaderComponent, decorators: [{
            type: Component,
            args: [{ selector: 'sof-qr-reader', template: "<div class=\"scanner-container\">\r\n  <h2>QR Scanner</h2>\r\n\r\n  <!-- Live Camera Scanner -->\r\n  <video #video class=\"qr-video\"></video>\r\n  <canvas #canvas class=\"qr-canvas\"></canvas>\r\n\r\n  <hr>\r\n\r\n  <!-- File Upload Scanner -->\r\n  <input type=\"file\" accept=\"image/*\" (change)=\"scanFile($event)\">\r\n  <canvas #fileCanvas class=\"qr-file-canvas\"></canvas>\r\n\r\n  @if (qrResult) {\r\n    <p>Scanned QR Code: {{ qrResult }}</p>\r\n  }\r\n</div>\r\n", styles: [".scanner-container{text-align:center;width:100%;max-width:500px;margin:auto}.qr-video{width:100%;height:auto;border:2px solid #000}.qr-canvas,.qr-file-canvas{display:none}input[type=file]{margin:10px 0}\n"] }]
        }], ctorParameters: () => [{ type: i1.Router }], propDecorators: { videoElement: [{
                type: ViewChild,
                args: ['video', { static: true }]
            }], canvasElement: [{
                type: ViewChild,
                args: ['canvas', { static: true }]
            }], fileCanvasElement: [{
                type: ViewChild,
                args: ['fileCanvas', { static: true }]
            }] } });

class LoginComponent {
    get patientQuery() { return this._patientQuery; }
    set patientQuery(value) {
        this._patientQuery = value;
        this.searchPatients();
    }
    hasPreviousPage() { return this.page > 1; }
    hasNextPage() { return this.page < (this.total / 10); }
    constructor(config, auth, sof, router) {
        this.config = config;
        this.auth = auth;
        this.sof = sof;
        this.router = router;
        // for Smart Health Card QR scanner
        this.scanning = false;
        // patient selection for public clients
        this.patientSelection = false;
        this.patientsLoading = false;
        this.patients = [];
        this.total = 0;
        this.page = 1;
        this._patientQuery = '';
    }
    async login(config) {
        if (config.isPublic) {
            this.client = this.auth.publicClient(config.iss);
            this.searchPatients();
            this.patientSelection = true;
            this.selectedClient = config;
        }
        else {
            this.auth.login(config);
        }
    }
    /**
     * Public client patient selection
     * @param patient
     */
    selectPatient(patient) {
        if (this.selectedClient) {
            this.auth.login(this.selectedClient, patient);
            this.router.navigate(['/']);
        }
    }
    // public client patient search methods
    next() { this.page += 1; this.searchPatients(); }
    prev() {
        this.page = this.page > 1 ? this.page - 1 : 1;
        this.searchPatients();
    }
    async searchPatients() {
        this.patientsLoading = true;
        let query = 'Patient?_count=10&_page=' + this.page;
        if (this.patientQuery?.trim()) {
            query += '&' + this.patientQuery.trim().split(' ').map(name => 'name:contains=' + name).join('&');
        }
        const bundle = await this.client?.request(query);
        if (bundle) {
            this.patients = bundle.entry?.map(entry => entry.resource)
                .filter(res => !!res) || [];
            this.total = bundle.total || 0;
            if ((this.page - 1) * 10 > this.total) {
                this.page = 1;
            }
        }
        else {
            this.patients = [];
            this.total = 0;
            this.page = 1;
        }
        this.patientsLoading = false;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoginComponent, deps: [{ token: 'sofConfig' }, { token: SmartAuthService }, { token: SmartOnFhirService }, { token: i1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: LoginComponent, selector: "sof-login", ngImport: i0, template: "<div class=\"sof-height-100vh\">\r\n  @if (config.logo || config.title) {\r\n    <div class=\"logo-n-title\">\r\n      @if (config.logo) {\r\n        <img [src]=\"config.logo\" class=\"logo\">\r\n      }\r\n      @if (config.title) {\r\n        <div class=\"title sof-text-light\" [style.width]=\"config.title.length / 2 + 'em'\">{{ config.title }}</div>\r\n      }\r\n    </div>\r\n  }\r\n  <div class=\"login-container sof-primary-background sof-login-background\">\r\n    <div class=\"sof-shadow sof-no-border sof-card sof-width-25 sof-margin-auto\">\r\n      <div class=\"sof-card-header sof-login-card-header\"><h4>Login via...</h4></div>\r\n      <div class=\"sof-card-body sof-text-center sof-light-background\">\r\n        @if (!scanning && !patientSelection) {\r\n          <!--    <h4>Login With:</h4>-->\r\n          @for (client of config?.loginClients; track $index) {\r\n            @if (client.image) {\r\n              <a (click)=\"login(client)\" style=\"cursor:pointer;\" class=\"sof-login-client-w-image image\">\r\n                <img [src]=\"client.image\">\r\n              </a>\r\n            } @else {\r\n              <button class=\"sof-button sof-text-bold\" [class.sof-button-light]=\"!client.background\" [class.sof-text-dark]=\"!client.color\"\r\n                      [ngStyle]=\"{background: client.background}\" (click)=\"login(client)\">\r\n                <div class=\"text-content\" [ngStyle]=\"{color: client.color}\">{{client.label}}</div>\r\n              </button>\r\n            }\r\n          }\r\n          @for (client of config?.launchClients; track $index) {\r\n            @if (client.image) {\r\n              <a [href]=\"client.url\" class=\"image sof-button sof-button-light\" style=\"cursor:pointer;\">\r\n                <img [src]=\"client.image\">\r\n              </a>\r\n            }\r\n          }\r\n          @for (client of config?.launchClients; track $index) {\r\n            @if (!client.image) {\r\n              <a class=\"sof-button sof-button-secondary sof-button-login sof-text-bold\" [href]=\"client.url\">{{client.label}}</a>\r\n            }\r\n          }\r\n          @if (config?.shcLoginEnabled) {\r\n            <!--          <span class=\"mt-5\">or</span>-->\r\n            <a class=\"sof-button sof-button-secondary\" (click)=\"scanning = true\">\r\n              <i class=\"sof-icon sof-icon-qr-code\"></i> SMART Health Card\r\n            </a>\r\n          }\r\n        } @else if (scanning) {\r\n          <sof-qr-reader #reader></sof-qr-reader>\r\n          @if (reader.error) {\r\n            <div class=\"sof-alert sof-alert-danger\">\r\n              {{ reader.error }}\r\n            </div>\r\n          }\r\n          <button class=\"sof-button sof-button-danger\" (click)=\"scanning = false\">Cancel</button>\r\n        } @else if (patientSelection) {\r\n          <div class=\"sof-width-100\" style=\"max-height: 50vh; overflow-y: auto\">\r\n            <table class=\"sof-table\">\r\n              <tbody>\r\n              <tr>\r\n                <td>\r\n                  <button class=\"sof-button sof-button-danger sof-mr-1\" (click)=\"patientSelection = false\">Cancel</button>\r\n                  <button class=\"sof-button sof-button-primary sof-mr-1\" [class.disabled]=\"!hasPreviousPage()\" (click)=\"prev()\"><i class=\"sof-icon sof-icon-caret-left\"></i></button>\r\n                  <input class=\"sof-form-control\" style=\"display: inline-block; width: auto\" placeholder=\"Search\" [(ngModel)]=\"patientQuery\">\r\n                  <button class=\"sof-button sof-button-primary sof-ml-1\" [class.disabled]=\"!hasNextPage()\" (click)=\"next()\"><i class=\"sof-icon sof-icon-caret-right\"></i></button>\r\n                </td>\r\n              </tr>\r\n              @if(patientsLoading) {\r\n                <tr><td>\r\n                  <div class=\"sof-spinner\" role=\"status\">\r\n                    <span class=\"sof-hidden\">Loading...</span>\r\n                  </div>\r\n                </td></tr>\r\n              } @else {\r\n                @for (patient of patients; track patient.id) {\r\n                  <tr style=\"cursor: pointer\" (click)=\"selectPatient(patient)\"><td>{{patient.name?.at(0)?.given}} {{patient.name?.at(0)?.family}}</td></tr>\r\n                }\r\n                <tr><td class=\"sof-text-bold sof-text-gray\" style=\"font-size: .75em\">{{(page - 1) * patients.length}} - {{page * patients.length}}/{{total}}</td></tr>\r\n              }\r\n              </tbody>\r\n            </table>\r\n          </div>\r\n        }\r\n      </div>\r\n    </div>\r\n  </div>\r\n</div>\r\n", styles: [".login-container{margin:-1rem!important;width:calc(100% + 2rem)!important;height:calc(100% + 2rem)!important}.login-container>.sof-card{min-width:500px;position:relative;top:50%;transform:translateY(-50%);min-height:500px}.login-container>.sof-card .sof-card-body{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-around;gap:1em;align-items:stretch}.login-container>.sof-card .sof-card-body>a.image{display:flex;justify-content:space-around;align-items:center}.login-container>.sof-card .sof-card-body>a.image img{width:150px}.login-container>.sof-card .sof-card-body>button .text-content{width:150px;white-space:wrap}.login-container>.sof-card .sof-card-body>a:not(.image){width:100%;display:flex;text-align:center;align-items:center;justify-content:center;gap:.5em}.logo-n-title{position:fixed;left:2em;top:2em;width:calc(100vw - 4em);display:flex;gap:1em}.logo-n-title .logo{width:150px}.logo-n-title .title{max-width:calc(85vw - 150px);font-size:2em;font-weight:700;border-left:.1em solid;padding-left:.4em;display:flex;align-items:center;white-space:break-spaces}\n"], dependencies: [{ kind: "directive", type: i5.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i5$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: QrReaderComponent, selector: "sof-qr-reader" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoginComponent, decorators: [{
            type: Component,
            args: [{ selector: 'sof-login', template: "<div class=\"sof-height-100vh\">\r\n  @if (config.logo || config.title) {\r\n    <div class=\"logo-n-title\">\r\n      @if (config.logo) {\r\n        <img [src]=\"config.logo\" class=\"logo\">\r\n      }\r\n      @if (config.title) {\r\n        <div class=\"title sof-text-light\" [style.width]=\"config.title.length / 2 + 'em'\">{{ config.title }}</div>\r\n      }\r\n    </div>\r\n  }\r\n  <div class=\"login-container sof-primary-background sof-login-background\">\r\n    <div class=\"sof-shadow sof-no-border sof-card sof-width-25 sof-margin-auto\">\r\n      <div class=\"sof-card-header sof-login-card-header\"><h4>Login via...</h4></div>\r\n      <div class=\"sof-card-body sof-text-center sof-light-background\">\r\n        @if (!scanning && !patientSelection) {\r\n          <!--    <h4>Login With:</h4>-->\r\n          @for (client of config?.loginClients; track $index) {\r\n            @if (client.image) {\r\n              <a (click)=\"login(client)\" style=\"cursor:pointer;\" class=\"sof-login-client-w-image image\">\r\n                <img [src]=\"client.image\">\r\n              </a>\r\n            } @else {\r\n              <button class=\"sof-button sof-text-bold\" [class.sof-button-light]=\"!client.background\" [class.sof-text-dark]=\"!client.color\"\r\n                      [ngStyle]=\"{background: client.background}\" (click)=\"login(client)\">\r\n                <div class=\"text-content\" [ngStyle]=\"{color: client.color}\">{{client.label}}</div>\r\n              </button>\r\n            }\r\n          }\r\n          @for (client of config?.launchClients; track $index) {\r\n            @if (client.image) {\r\n              <a [href]=\"client.url\" class=\"image sof-button sof-button-light\" style=\"cursor:pointer;\">\r\n                <img [src]=\"client.image\">\r\n              </a>\r\n            }\r\n          }\r\n          @for (client of config?.launchClients; track $index) {\r\n            @if (!client.image) {\r\n              <a class=\"sof-button sof-button-secondary sof-button-login sof-text-bold\" [href]=\"client.url\">{{client.label}}</a>\r\n            }\r\n          }\r\n          @if (config?.shcLoginEnabled) {\r\n            <!--          <span class=\"mt-5\">or</span>-->\r\n            <a class=\"sof-button sof-button-secondary\" (click)=\"scanning = true\">\r\n              <i class=\"sof-icon sof-icon-qr-code\"></i> SMART Health Card\r\n            </a>\r\n          }\r\n        } @else if (scanning) {\r\n          <sof-qr-reader #reader></sof-qr-reader>\r\n          @if (reader.error) {\r\n            <div class=\"sof-alert sof-alert-danger\">\r\n              {{ reader.error }}\r\n            </div>\r\n          }\r\n          <button class=\"sof-button sof-button-danger\" (click)=\"scanning = false\">Cancel</button>\r\n        } @else if (patientSelection) {\r\n          <div class=\"sof-width-100\" style=\"max-height: 50vh; overflow-y: auto\">\r\n            <table class=\"sof-table\">\r\n              <tbody>\r\n              <tr>\r\n                <td>\r\n                  <button class=\"sof-button sof-button-danger sof-mr-1\" (click)=\"patientSelection = false\">Cancel</button>\r\n                  <button class=\"sof-button sof-button-primary sof-mr-1\" [class.disabled]=\"!hasPreviousPage()\" (click)=\"prev()\"><i class=\"sof-icon sof-icon-caret-left\"></i></button>\r\n                  <input class=\"sof-form-control\" style=\"display: inline-block; width: auto\" placeholder=\"Search\" [(ngModel)]=\"patientQuery\">\r\n                  <button class=\"sof-button sof-button-primary sof-ml-1\" [class.disabled]=\"!hasNextPage()\" (click)=\"next()\"><i class=\"sof-icon sof-icon-caret-right\"></i></button>\r\n                </td>\r\n              </tr>\r\n              @if(patientsLoading) {\r\n                <tr><td>\r\n                  <div class=\"sof-spinner\" role=\"status\">\r\n                    <span class=\"sof-hidden\">Loading...</span>\r\n                  </div>\r\n                </td></tr>\r\n              } @else {\r\n                @for (patient of patients; track patient.id) {\r\n                  <tr style=\"cursor: pointer\" (click)=\"selectPatient(patient)\"><td>{{patient.name?.at(0)?.given}} {{patient.name?.at(0)?.family}}</td></tr>\r\n                }\r\n                <tr><td class=\"sof-text-bold sof-text-gray\" style=\"font-size: .75em\">{{(page - 1) * patients.length}} - {{page * patients.length}}/{{total}}</td></tr>\r\n              }\r\n              </tbody>\r\n            </table>\r\n          </div>\r\n        }\r\n      </div>\r\n    </div>\r\n  </div>\r\n</div>\r\n", styles: [".login-container{margin:-1rem!important;width:calc(100% + 2rem)!important;height:calc(100% + 2rem)!important}.login-container>.sof-card{min-width:500px;position:relative;top:50%;transform:translateY(-50%);min-height:500px}.login-container>.sof-card .sof-card-body{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-around;gap:1em;align-items:stretch}.login-container>.sof-card .sof-card-body>a.image{display:flex;justify-content:space-around;align-items:center}.login-container>.sof-card .sof-card-body>a.image img{width:150px}.login-container>.sof-card .sof-card-body>button .text-content{width:150px;white-space:wrap}.login-container>.sof-card .sof-card-body>a:not(.image){width:100%;display:flex;text-align:center;align-items:center;justify-content:center;gap:.5em}.logo-n-title{position:fixed;left:2em;top:2em;width:calc(100vw - 4em);display:flex;gap:1em}.logo-n-title .logo{width:150px}.logo-n-title .title{max-width:calc(85vw - 150px);font-size:2em;font-weight:700;border-left:.1em solid;padding-left:.4em;display:flex;align-items:center;white-space:break-spaces}\n"] }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: ['sofConfig']
                }] }, { type: SmartAuthService }, { type: SmartOnFhirService }, { type: i1.Router }] });

class LaunchComponent {
    constructor(config, route, auth) {
        this.config = config;
        this.route = route;
        this.auth = auth;
        // Handle the query parameters to be used in the Smart App Launch flow
        this.route.queryParams.subscribe(params => {
            const iss = decodeURIComponent(params['iss']);
            const launch = params['launch'];
            // start launch flow
            this.auth.launch(iss, launch);
        });
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LaunchComponent, deps: [{ token: 'sofConfig' }, { token: i1.ActivatedRoute }, { token: SmartAuthService }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LaunchComponent, selector: "sof-launch", ngImport: i0, template: "<div class=\"w-100 h-100 position-fixed bg-light\" style=\"line-height: 100vh; text-align: center\">\r\n  <span class=\"spinner-border spinner-border-sm\" aria-hidden=\"true\"></span> Processing Authorization...\r\n</div>\r\n", styles: [""] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LaunchComponent, decorators: [{
            type: Component,
            args: [{ selector: 'sof-launch', template: "<div class=\"w-100 h-100 position-fixed bg-light\" style=\"line-height: 100vh; text-align: center\">\r\n  <span class=\"spinner-border spinner-border-sm\" aria-hidden=\"true\"></span> Processing Authorization...\r\n</div>\r\n" }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: ['sofConfig']
                }] }, { type: i1.ActivatedRoute }, { type: SmartAuthService }] });

class CallbackComponent {
    constructor(router, route, auth) {
        this.router = router;
        this.route = route;
        this.auth = auth;
    }
    ngOnInit() {
        // Subscribe to route data to decide redirection URL after successful authentication
        this.route.data.subscribe(data => {
            // Call authentication service to handle code/token exchange
            this.auth.start().then(() => {
                // redirect to specified page after the token is successfully retrieved
                this.router.navigate([data['redirectTo']]);
            }, (error) => {
                // set error message if failed
                this.error = error?.message || error?.toString() || 'Unknown error occurred.';
            });
        });
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CallbackComponent, deps: [{ token: i1.Router }, { token: i1.ActivatedRoute }, { token: SmartAuthService }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CallbackComponent, selector: "sof-callback", ngImport: i0, template: "<div class=\"sof-width-100 sof-height-100 sof-light-background\" style=\"position: fixed; line-height: 100vh; text-align: center\">\r\n  @if (!error) {\r\n    <span class=\"sof-spinner\" style=\"vertical-align: middle\" aria-hidden=\"true\"></span> Processing Authorization...\r\n  } @else {\r\n    <div style=\"display: inline-block; line-height: 2em\">\r\n      <i class=\"sof-text-danger sof-icon sof-icon-exclamation\" style=\"font-size: 2em\"></i>\r\n      <p>{{error}}asdsadasdasd</p>\r\n    </div>\r\n  }\r\n</div>\r\n", styles: [""] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CallbackComponent, decorators: [{
            type: Component,
            args: [{ selector: 'sof-callback', template: "<div class=\"sof-width-100 sof-height-100 sof-light-background\" style=\"position: fixed; line-height: 100vh; text-align: center\">\r\n  @if (!error) {\r\n    <span class=\"sof-spinner\" style=\"vertical-align: middle\" aria-hidden=\"true\"></span> Processing Authorization...\r\n  } @else {\r\n    <div style=\"display: inline-block; line-height: 2em\">\r\n      <i class=\"sof-text-danger sof-icon sof-icon-exclamation\" style=\"font-size: 2em\"></i>\r\n      <p>{{error}}asdsadasdasd</p>\r\n    </div>\r\n  }\r\n</div>\r\n" }]
        }], ctorParameters: () => [{ type: i1.Router }, { type: i1.ActivatedRoute }, { type: SmartAuthService }] });

class CamelCaseSpacedPipe {
    transform(value) {
        return value.replaceAll(/([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g, '$1$4 $2$3$5');
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CamelCaseSpacedPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: CamelCaseSpacedPipe, name: "camelCaseSpaced" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CamelCaseSpacedPipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'camelCaseSpaced'
                }]
        }] });

/**
 * SMART Health Link Service
 * Executes the requests, parses and validates SMART Health Links as defined in the HL7 documentations
 */
class ShlService {
    get emptyBundle() {
        return {
            resourceType: 'Bundle',
            type: 'document',
            entry: []
        };
    }
    constructor(http) {
        this.http = http;
    }
    async handleShl(shl) {
        if (shl.startsWith('shlink:/')) {
            shl = shl.slice('shlink:/'.length);
        }
        const metadata = JSON.parse(atob(shl));
        if (!metadata.url) {
            throw new Error('URL is missing in SMART Health Link');
        }
        let payload;
        if (metadata.flag?.includes('U')) {
            payload = await firstValueFrom(this.http.get(metadata.url + '?recipient=' + window.location.origin))
                .then((data) => data.files.filter((file) => file.contentType === 'application/smart-health-card' && file.embedded).map((file) => file.embedded));
        }
        else {
            let passcode;
            if (metadata.flag?.includes('P')) {
                passcode = prompt('Enter the passcode for SMART Health Link');
            }
            payload = await firstValueFrom(this.http.post(metadata.url, {
                recipient: window.location.origin,
                passcode
            })).then(async (data) => {
                const embedded = data.files.filter((file) => file.contentType === 'application/smart-health-card' && file.embedded).map((file) => file.embedded);
                const links = data.files.filter((file) => file.contentType === 'application/fhir+json' && file.location);
                const requests = links.map((file) => firstValueFrom(this.http.get(file.location, { responseType: 'text' })));
                const linkedCards = await Promise.all(requests);
                return [...embedded, ...linkedCards];
            });
        }
        if (!payload) {
            throw new Error('Cannot get SMART Health Link content.');
        }
        const shcs = await this.decryptSHLPayloadAndGetSHCs(payload, metadata.key);
        const bundles = await Promise.all(shcs.map(shc => this.getSingleBundle(shc)));
        return {
            label: metadata.label,
            shc: shcs,
            verified: await Promise.all(shcs.map(_shc => this.verifyShc(_shc))).then(results => results.every(value => value)),
            bundle: Object.assign(this.emptyBundle, {
                entry: bundles.filter(_ => _).map(bundle => bundle.entry).reduce((arr, entries) => arr.concat(entries), [])
            })
        };
    }
    async decryptSHLPayloadAndGetSHCs(payload, key) {
        const SHCs = [];
        for (const encrypted of payload) {
            const data = await jose.compactDecrypt(encrypted, jose.base64url.decode(key));
            SHCs.push(...JSON.parse(new TextDecoder().decode(data.plaintext)).verifiableCredential);
        }
        return SHCs;
    }
    async handleShc(shc) {
        if (shc.startsWith('shc:/')) {
            shc = shc.slice('shc:/'.length);
        }
        return {
            shc: [shc],
            bundle: await this.getSingleBundle(shc),
            verified: await this.verifyShc(shc)
        };
    }
    async getSingleBundle(shc) {
        return await this.parseShc(shc).then(data => data.vc.credentialSubject.fhirBundle);
    }
    async parseShc(shc) {
        try {
            const [h, p, s] = shc.split('.').map((part) => this.base64UrlToBytes(part));
            return JSON.parse(this.inflate(p));
        }
        catch (error) {
            console.error('Error parsing SMART Health Card:', error);
            throw error;
        }
    }
    async verifyShc(shc) {
        try {
            await jose.compactVerify(shc, jose.createLocalJWKSet(await this.fetchJWKs(shc)));
            return true;
        }
        catch (err) {
            return false;
        }
    }
    async fetchJWKs(shc) {
        try {
            const card = JSON.parse(this.inflate(this.base64UrlToBytes(shc.split('.')[1])));
            const issuer = card.iss;
            const jwksUrl = `${issuer.replace(/\/$/, '')}/.well-known/jwks`;
            const jwksResponse = await firstValueFrom(this.http.get(jwksUrl));
            return jwksResponse;
        }
        catch (error) {
            console.error('Error fetching JWKs:', error);
            throw new Error('Unable to fetch JWKs for signature verification');
        }
    }
    inflate(data) {
        try {
            const decompressed = pako.inflateRaw(data, { to: 'string' });
            return decompressed;
        }
        catch (error) {
            console.error('Error inflating data:', error);
            throw new Error('Failed to inflate data');
        }
    }
    base64UrlToBytes(base64Url) {
        const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
        const binaryString = atob(base64);
        return new Uint8Array([...binaryString].map((char) => char.charCodeAt(0)));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShlService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShlService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShlService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root'
                }]
        }], ctorParameters: () => [{ type: i1$1.HttpClient }] });

/**
 * Component for searching and validating Smart Health Cards or Smart Health Links
 */
class ShcShlHandlerComponent {
    get patientName() {
        return this.patient?.name?.map(name => (name.given?.join(' ') || '') + ' ' + name.family).join(', ');
    }
    get patientAge() {
        if (this.patient?.birthDate) {
            return Math.ceil((Date.now() - new Date(this.patient.birthDate).getTime()) / (1000 * 3600 * 24 * 365));
        }
        else {
            return '';
        }
    }
    constructor(route, auth, shl, sof) {
        this.route = route;
        this.auth = auth;
        this.shl = shl;
        this.sof = sof;
        this.invalidSignature = false;
        this.referenceMap = {};
        this.auth.getPatient().then(patient => this.patient = patient);
        route.fragment.subscribe(async (fragment) => {
            try {
                if (fragment?.startsWith('shlink:/')) {
                    this.sof.importSHCData(await this.shl.handleShl(fragment));
                    this.checkIPS();
                }
                else if (fragment?.startsWith('shc:/')) {
                    this.sof.importSHCData(await this.shl.handleShc(fragment));
                    this.checkIPS();
                }
                else {
                    console.log("No SHC data in the fragment:", fragment);
                }
            }
            catch (error) {
                if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
                    this.invalidSignature = true;
                }
            }
        });
    }
    ngOnInit() {
    }
    logout() {
        this.sof.logout();
    }
    checkIPS() {
        if (this.sof.importedResources['Composition']) {
            const ips = this.sof.importedResources['Composition']
                .find((resource) => resource.type?.coding?.some(code => code.code === '60591-5'));
            if (ips) {
                this.referenceMap = this.sof.getAllImportedResources().reduce((map, resource) => {
                    map[resource.resourceType + '/' + resource.id] = resource;
                    return map;
                }, {});
                this.ips = ips;
            }
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShcShlHandlerComponent, deps: [{ token: i1.ActivatedRoute }, { token: SmartAuthService }, { token: ShlService }, { token: SmartOnFhirService }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ShcShlHandlerComponent, selector: "sof-shc-shl-handler", ngImport: i0, template: `
    <div class="container">
      @if (invalidSignature) {
        <div class="sof-alert sof-alert-danger">
          Issuer verification failed for the Smart Health Card!
        </div>
        <button class="sof-button sof-button-danger" (click)="logout()"><i class="sof-icon sof-icon-caret-left"></i> Back</button>
        <button class="sof-button sof-button-warning sof-ml-1 sof-mt-1" (click)="invalidSignature = false"><i class="sof-icon sof-icon-exclamation"></i> Ignore and Continue</button>
      } @else {
        <table class="sof-table" id="shc-content">
          <tbody>
          <tr>
            <td><b>Patient</b></td>
            <td>{{ patientName }}</td>
          </tr>
          <tr>
            <td><b>Age</b></td>
            <td>{{ patientAge }}</td>
          </tr>
          <tr>
            <td><b>Gender</b></td>
            <td>{{ patient?.gender }}</td>
          </tr>
          @if (ips) {
            <tr>
              <td colspan="2"><h4>International Patient Summary Imported</h4></td>
            </tr>
            @for (section of ips.section; track section.title) {
              <tr>
                <td colspan="2">
                  <h5>{{section.title}}</h5>
                </td>
              </tr>
              <ng-template #myTemplate let-resource>
                @if (resource) {
                  @switch (resource.resourceType) {
                    @case ('Condition') {
                      <tr>
                        <td>{{resource.code?.coding?.at(0)?.display || resource.code?.coding?.at(0)?.code || resource.code?.text}}</td>
                        <td class="text-end">
                          {{resource.onsetDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('MedicationStatement') {
                      <tr>
                        <td>{{resource.medicationCodeableConcept?.coding?.at(0)?.display || resource.medicationCodeableConcept?.coding?.at(0)?.code || resource.medicationCodeableConcept?.text}}</td>
                        <td class="text-end">
                          {{resource.effectiveDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('AllergyIntolerance') {
                      <tr>
                        <td>{{resource.code?.coding?.at(0)?.display || resource.code?.coding?.at(0)?.code || resource.code?.text}}</td>
                        <td class="text-end">
                          {{resource.onsetDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('Immunization') {
                      <tr>
                        <td>{{resource.vaccineCode?.coding?.at(0)?.display || resource.vaccineCode?.coding?.at(0)?.code || resource.vaccineCode?.text}}</td>
                        <td class="text-end">
                          {{resource.occurrenceDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('Observation') {
                      <tr>
                        <td>{{resource.code?.coding?.at(0)?.display || resource.code?.coding?.at(0)?.code || resource.code?.text}}</td>
                        <td class="text-end">
                          {{resource.effectiveDateTime | date}}
                          <br>
                          @if (resource.valueQuantity) {
                            {{resource.valueQuantity.value}} {{resource.valueQuantity.unit || resource.valueQuantity.code}}
                          }
                        </td>
                      </tr>
                    }
                  }
                }
              </ng-template>
              @for (entry of section.entry; track entry.reference) {
                <ng-container *ngTemplateOutlet="myTemplate; context: { $implicit: referenceMap[entry.reference || ''] }"></ng-container>
              }
            }
          } @else {
            @for (resources of sof.importedResources | keyvalue; track resources.key) {
              <tr>
                <td><b>{{resources.key | camelCaseSpaced}}</b></td>
                <td><span class="sof-badge sof-primary-background sof-text-light">{{resources.value?.length}}</span></td>
              </tr>
            }
          }
          <tr>
            <td colspan="2" class="text-end">
              <button class="sof-button sof-button-primary" routerLink="/">Continue <i class="sof-icon sof-icon-caret-right"></i></button>
            </td>
          </tr>
          </tbody>
        </table>
      }
    </div>
    <style>
      #shc-content td:first-child {
        width: 25em;
        max-width: 100%;
      }
    </style>
  `, isInline: true, styles: ["\n      #shc-content td:first-child {\n        width: 25em;\n        max-width: 100%;\n      }\n    "], dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i5.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i5.KeyValuePipe, name: "keyvalue" }, { kind: "pipe", type: i5.DatePipe, name: "date" }, { kind: "pipe", type: CamelCaseSpacedPipe, name: "camelCaseSpaced" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShcShlHandlerComponent, decorators: [{
            type: Component,
            args: [{
                    selector: 'sof-shc-shl-handler',
                    template: `
    <div class="container">
      @if (invalidSignature) {
        <div class="sof-alert sof-alert-danger">
          Issuer verification failed for the Smart Health Card!
        </div>
        <button class="sof-button sof-button-danger" (click)="logout()"><i class="sof-icon sof-icon-caret-left"></i> Back</button>
        <button class="sof-button sof-button-warning sof-ml-1 sof-mt-1" (click)="invalidSignature = false"><i class="sof-icon sof-icon-exclamation"></i> Ignore and Continue</button>
      } @else {
        <table class="sof-table" id="shc-content">
          <tbody>
          <tr>
            <td><b>Patient</b></td>
            <td>{{ patientName }}</td>
          </tr>
          <tr>
            <td><b>Age</b></td>
            <td>{{ patientAge }}</td>
          </tr>
          <tr>
            <td><b>Gender</b></td>
            <td>{{ patient?.gender }}</td>
          </tr>
          @if (ips) {
            <tr>
              <td colspan="2"><h4>International Patient Summary Imported</h4></td>
            </tr>
            @for (section of ips.section; track section.title) {
              <tr>
                <td colspan="2">
                  <h5>{{section.title}}</h5>
                </td>
              </tr>
              <ng-template #myTemplate let-resource>
                @if (resource) {
                  @switch (resource.resourceType) {
                    @case ('Condition') {
                      <tr>
                        <td>{{resource.code?.coding?.at(0)?.display || resource.code?.coding?.at(0)?.code || resource.code?.text}}</td>
                        <td class="text-end">
                          {{resource.onsetDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('MedicationStatement') {
                      <tr>
                        <td>{{resource.medicationCodeableConcept?.coding?.at(0)?.display || resource.medicationCodeableConcept?.coding?.at(0)?.code || resource.medicationCodeableConcept?.text}}</td>
                        <td class="text-end">
                          {{resource.effectiveDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('AllergyIntolerance') {
                      <tr>
                        <td>{{resource.code?.coding?.at(0)?.display || resource.code?.coding?.at(0)?.code || resource.code?.text}}</td>
                        <td class="text-end">
                          {{resource.onsetDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('Immunization') {
                      <tr>
                        <td>{{resource.vaccineCode?.coding?.at(0)?.display || resource.vaccineCode?.coding?.at(0)?.code || resource.vaccineCode?.text}}</td>
                        <td class="text-end">
                          {{resource.occurrenceDateTime | date}}
                        </td>
                      </tr>
                    }
                    @case ('Observation') {
                      <tr>
                        <td>{{resource.code?.coding?.at(0)?.display || resource.code?.coding?.at(0)?.code || resource.code?.text}}</td>
                        <td class="text-end">
                          {{resource.effectiveDateTime | date}}
                          <br>
                          @if (resource.valueQuantity) {
                            {{resource.valueQuantity.value}} {{resource.valueQuantity.unit || resource.valueQuantity.code}}
                          }
                        </td>
                      </tr>
                    }
                  }
                }
              </ng-template>
              @for (entry of section.entry; track entry.reference) {
                <ng-container *ngTemplateOutlet="myTemplate; context: { $implicit: referenceMap[entry.reference || ''] }"></ng-container>
              }
            }
          } @else {
            @for (resources of sof.importedResources | keyvalue; track resources.key) {
              <tr>
                <td><b>{{resources.key | camelCaseSpaced}}</b></td>
                <td><span class="sof-badge sof-primary-background sof-text-light">{{resources.value?.length}}</span></td>
              </tr>
            }
          }
          <tr>
            <td colspan="2" class="text-end">
              <button class="sof-button sof-button-primary" routerLink="/">Continue <i class="sof-icon sof-icon-caret-right"></i></button>
            </td>
          </tr>
          </tbody>
        </table>
      }
    </div>
    <style>
      #shc-content td:first-child {
        width: 25em;
        max-width: 100%;
      }
    </style>
  `
                }]
        }], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: SmartAuthService }, { type: ShlService }, { type: SmartOnFhirService }] });

class SmartOnFhirModule {
    static forRoot(config) {
        return {
            ngModule: SmartOnFhirModule,
            providers: [SmartOnFhirService, { provide: 'sofConfig', useValue: config }, SmartAuthService]
        };
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
    static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, declarations: [LoginComponent, LaunchComponent, CallbackComponent, QrReaderComponent, CamelCaseSpacedPipe, ShcShlHandlerComponent], imports: [NgStyle, KeyValuePipe, RouterModule, AsyncPipe, FormsModule, CommonModule] }); }
    static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, providers: [SmartOnFhirService, SmartAuthService], imports: [RouterModule, FormsModule, CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, decorators: [{
            type: NgModule,
            args: [{
                    imports: [
                        NgStyle, KeyValuePipe, RouterModule, AsyncPipe, FormsModule, CommonModule
                    ],
                    exports: [],
                    declarations: [LoginComponent, LaunchComponent, CallbackComponent, QrReaderComponent, CamelCaseSpacedPipe, ShcShlHandlerComponent],
                    providers: [SmartOnFhirService, SmartAuthService],
                }]
        }] });

const redirectUnauthorizedToLogin = async function (route, state) {
    const router = inject(Router);
    const auth = inject(SmartAuthService);
    const sof = inject(SmartOnFhirService);
    const shl = inject(ShlService);
    if (auth.isLoggedIn()) {
        return true;
    }
    try {
        const context = auth.restoreSession();
        if (context?.shc) {
            await Promise.all(context.shc.map(async (shc) => sof.importSHCData(await shl.handleShc(shc))));
        }
        await auth.start();
        return auth.isLoggedIn();
    }
    catch (err) { }
    if (auth.isLoggedIn()) {
        return true;
    }
    await router.navigate(['/login']);
    return false;
};

/**
 * Routes wrapper to add required components to execute Smart App Launch and Login flows
 * @param routes - Other application routes
 * @param redirectTo - Redirection after login
 * @param method - Allowed authentication flows: launch/client(login)/both
 * @param redirectToLoginIfUnauthorized - If true, adds auth guards to the application components
 * @param enableShc - If true, offline mode is allowed using Smart Health Cards
 */
const withSmartHandlerRoutes = (routes, redirectTo, method, redirectToLoginIfUnauthorized, enableShc) => {
    const smartRoutes = [{
            path: 'callback',
            component: CallbackComponent,
            data: { redirectTo }
        }];
    if (method !== 'client') {
        smartRoutes.push({
            path: 'launch',
            component: LaunchComponent
        });
    }
    if (method !== 'launch') {
        smartRoutes.push({
            path: 'login',
            component: LoginComponent
        });
    }
    if (enableShc) {
        smartRoutes.push({
            path: 'shl',
            component: ShcShlHandlerComponent
        }, {
            path: 'shc',
            component: ShcShlHandlerComponent
        });
    }
    return [
        ...routes.map(route => {
            if (redirectToLoginIfUnauthorized) {
                route.canActivate = (route.canActivate || []).concat([redirectUnauthorizedToLogin]);
            }
            return route;
        }),
        ...smartRoutes
    ];
};

/*
 * Public API Surface of ng-smart-on-fhir
 */

/**
 * Generated bundle index. Do not edit.
 */

export { SmartAuthService, SmartOnFhirModule, SmartOnFhirService, withSmartHandlerRoutes };
//# sourceMappingURL=ng-smart-on-fhir.mjs.map