• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

medplum / medplum / 28375420238

29 Jun 2026 01:25PM UTC coverage: 91.847% (-0.004%) from 91.851%
28375420238

push

github

web-flow
Add SMART Health Link import feature to Provider app (#9665)

* Add SMART Health Link import feature to Provider app

Signed-off-by: Cody Ebberson <cody@ebberson.com>

* [autofix.ci] apply automated fixes

* PR feedback

Signed-off-by: Cody Ebberson <cody@ebberson.com>

* Cleanup

Signed-off-by: Cody Ebberson <cody@ebberson.com>

* [autofix.ci] apply automated fixes

* PR feedback

Signed-off-by: Cody Ebberson <cody@ebberson.com>

* [autofix.ci] apply automated fixes

---------

Signed-off-by: Cody Ebberson <cody@ebberson.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

21371 of 24311 branches covered (87.91%)

Branch coverage included in aggregate %.

20 of 23 new or added lines in 1 file covered. (86.96%)

38234 of 40585 relevant lines covered (94.21%)

12547.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.31
/packages/react/src/QrCodeScanner/QrCodeScanner.tsx
1
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
// SPDX-License-Identifier: Apache-2.0
3
import { Alert, Loader, Stack, Text } from '@mantine/core';
4
import { normalizeErrorString } from '@medplum/core';
5
import type { JSX } from 'react';
6
import { useEffect, useRef, useState } from 'react';
7

8
const SCAN_INTERVAL_MS = 150;
2✔
9

10
type JsQr = (
11
  data: Uint8ClampedArray,
12
  width: number,
13
  height: number,
14
  options?: { inversionAttempts?: 'dontInvert' | 'onlyInvert' | 'attemptBoth' | 'invertFirst' }
15
) => { data: string } | null;
16

17
let jsQrPromise: Promise<JsQr> | undefined;
18

19
export interface QrCodeScannerProps {
20
  readonly onScan: (data: string) => void;
21
  readonly onError?: (error: Error) => void;
22
  readonly scanOnce?: boolean;
23
}
24

25
export function QrCodeScanner({ onScan, onError, scanOnce = true }: QrCodeScannerProps): JSX.Element {
13✔
26
  const videoRef = useRef<HTMLVideoElement>(null);
13✔
27
  const canvasRef = useRef<HTMLCanvasElement>(null);
13✔
28
  const onScanRef = useRef(onScan);
13✔
29
  const onErrorRef = useRef(onError);
13✔
30
  const [loading, setLoading] = useState(true);
13✔
31
  const [error, setError] = useState<string>();
13✔
32

33
  useEffect(() => {
13✔
34
    onScanRef.current = onScan;
8✔
35
  }, [onScan]);
36

37
  useEffect(() => {
13✔
38
    onErrorRef.current = onError;
8✔
39
  }, [onError]);
40

41
  useEffect(() => {
13✔
42
    const video = videoRef.current;
8✔
43
    const canvasElement = canvasRef.current;
8✔
44
    const ctx = canvasElement?.getContext('2d', { willReadFrequently: true });
8✔
45
    if (!video || !canvasElement || !ctx) {
8✔
46
      return undefined;
1✔
47
    }
48
    const videoElement = video;
7✔
49
    const scanCanvas = canvasElement;
7✔
50
    const scanCtx = ctx;
7✔
51

52
    let rafId = 0;
7✔
53
    let stream: MediaStream | undefined;
54
    let cancelled = false;
7✔
55
    let scanned = false;
7✔
56
    let lastScanTime = 0;
7✔
57
    let jsQr: JsQr | undefined;
58

59
    function stopStream(): void {
60
      stream?.getTracks().forEach((t) => t.stop());
8✔
61
    }
62

63
    function fail(err: unknown): void {
64
      const normalized = err instanceof Error ? err : new Error(String(err));
3✔
65
      if (!cancelled) {
3!
66
        setLoading(false);
3✔
67
        setError(normalizeErrorString(normalized));
3✔
68
        onErrorRef.current?.(normalized);
3✔
69
      }
70
    }
71

72
    function tick(timestamp: number): void {
73
      if (cancelled || scanned || !jsQr) {
3✔
74
        return;
1✔
75
      }
76
      if (videoElement.readyState === videoElement.HAVE_ENOUGH_DATA) {
2!
77
        setLoading(false);
2✔
78
        if (timestamp - lastScanTime >= SCAN_INTERVAL_MS) {
2!
79
          lastScanTime = timestamp;
2✔
80
          scanCanvas.height = videoElement.videoHeight;
2✔
81
          scanCanvas.width = videoElement.videoWidth;
2✔
82
          scanCtx.drawImage(videoElement, 0, 0, scanCanvas.width, scanCanvas.height);
2✔
83
          const imageData = scanCtx.getImageData(0, 0, scanCanvas.width, scanCanvas.height);
2✔
84
          const code = jsQr(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' });
2✔
85
          if (code?.data) {
2!
86
            scanned = scanOnce;
2✔
87
            onScanRef.current(code.data);
2✔
88
            if (scanOnce) {
2✔
89
              stopStream();
1✔
90
              return;
1✔
91
            }
92
          }
93
        }
94
      }
95
      rafId = requestAnimationFrame(tick);
1✔
96
    }
97

98
    async function start(): Promise<void> {
99
      try {
7✔
100
        jsQr = await loadJsQr();
7✔
101
        if (cancelled) {
7!
NEW
102
          return;
×
103
        }
104
        if (!navigator.mediaDevices?.getUserMedia) {
7✔
105
          throw new Error('Camera access is not available in this browser.');
1✔
106
        }
107
        const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
6✔
108
        if (cancelled) {
4✔
109
          mediaStream.getTracks().forEach((t) => t.stop());
1✔
110
          return;
1✔
111
        }
112
        stream = mediaStream;
3✔
113
        videoElement.srcObject = mediaStream;
3✔
114
        videoElement.setAttribute('playsinline', 'true');
3✔
115
        await videoElement.play();
3✔
116
        if (!cancelled) {
3!
117
          rafId = requestAnimationFrame(tick);
3✔
118
        }
119
      } catch (err) {
120
        fail(err);
3✔
121
      }
122
    }
123

124
    start().catch(fail);
7✔
125

126
    return () => {
7✔
127
      cancelled = true;
7✔
128
      cancelAnimationFrame(rafId);
7✔
129
      stopStream();
7✔
130
    };
131
  }, [scanOnce]);
132

133
  return (
13✔
134
    <Stack gap="sm">
135
      {error && <Alert color="red">{error}</Alert>}
16✔
136
      <video
137
        ref={videoRef}
138
        width={640}
139
        height={480}
140
        muted
141
        style={{
142
          aspectRatio: '4 / 3',
143
          display: error ? 'none' : 'block',
13✔
144
          width: '100%',
145
          height: 'auto',
146
          maxHeight: '70vh',
147
          background: 'black',
148
        }}
149
      />
150
      <canvas ref={canvasRef} hidden />
151
      {loading && !error && (
29✔
152
        <Text c="dimmed" size="sm">
153
          <Loader size="xs" mr="xs" />
154
          Loading camera...
155
        </Text>
156
      )}
157
    </Stack>
158
  );
159
}
160

161
async function loadJsQr(): Promise<JsQr> {
162
  if (!jsQrPromise) {
7✔
163
    jsQrPromise = import('jsqr')
1✔
164
      .then((module) => module.default as JsQr)
165
      .catch((err) => {
1✔
NEW
166
        jsQrPromise = undefined;
×
NEW
167
        throw new Error(
×
168
          `QR code scanning requires the optional "jsqr" dependency. Install jsqr to use QrCodeScanner. ${normalizeErrorString(
169
            err
170
          )}`
171
        );
172
      });
173
  }
174
  return jsQrPromise;
7✔
175
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc