You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

898 lines
27 KiB

<template>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<div class="mb-2">
<h1 class="text-2xl text-center font-semibold relative px-7">
<!-- Back -->
<a
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="handleBack"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</a>
<!-- Quick Help -->
<a
class="text-2xl text-center text-blue-500 px-2 py-1 absolute -right-2 -top-1"
@click="toastQRCodeHelp()"
>
<font-awesome icon="circle-question" class="fa-fw" />
</a>
Share Contact Info
</h1>
</div>
<div v-if="!givenName" :class="nameWarningClasses">
<p class="mb-2">
<b>Note:</b> your identity currently does <b>not</b> include a name.
</p>
<button :class="setNameButtonClasses" @click="openUserNameDialog">
Set Your Name
</button>
</div>
<UserNameDialog ref="userNameDialog" />
<div
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
:class="qrCodeContainerClasses"
@click="onCopyUrlToClipboard()"
>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="qrValue"
:width="606"
:height="606"
:corners-square-options="{ type: 'square' }"
:dots-options="{ type: 'square', color: '#000' }"
/>
</div>
<div v-else-if="activeDid" class="text-center my-4">
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
<span class="text-blue-500" @click="onCopyDidToClipboard()">
Click here to copy your DID to your clipboard.
</span>
<span>
Then give it to them so they can paste it in their list of People.
</span>
</div>
<div v-else class="text-center my-4">
You have no identitifiers yet, so
<router-link
:to="{ name: 'start' }"
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
create your identifier.
</router-link>
<br />
If you don't do that first, these contacts won't see your activity.
</div>
<div class="text-center mt-6">
<div v-if="isScanning" :class="scannerContainerClasses">
<!-- Status Message -->
<div :class="statusMessageClasses">
<div
v-if="cameraState === 'initializing'"
class="flex items-center justify-center space-x-2"
>
<svg
class="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0
3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span>{{ cameraStateMessage || "Initializing camera..." }}</span>
</div>
<p
v-else-if="cameraState === 'active'"
class="flex items-center justify-center space-x-2"
>
<span
class="inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"
></span>
<span>Position QR code in the frame</span>
</p>
<p v-else-if="error" class="text-red-400">Error: {{ error }}</p>
<p v-else class="flex items-center justify-center space-x-2">
<span :class="cameraStatusIndicatorClasses"></span>
<span>{{ cameraStateMessage || "Ready to scan" }}</span>
</p>
</div>
<qrcode-stream
v-if="useQRReader"
:camera="preferredCamera"
class="qr-scanner"
@decode="onDecode"
@init="onInit"
@detect="onDetect"
@error="onError"
@camera-on="onCameraOn"
@camera-off="onCameraOff"
/>
</div>
</div>
</section>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { QrcodeStream } from "vue-qrcode-reader";
import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import { NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import {
CONTACT_CSV_HEADER,
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
generateEndorserJwtUrlForAccount,
register,
setVisibilityUtil,
} from "../libs/endorserServer";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import * as libsUtil from "../libs/util";
import { Router } from "vue-router";
import { logger } from "../utils/logger";
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
import { CameraState } from "@/services/QRScanner/types";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { Account } from "@/db/tables/accounts";
import { createNotifyHelpers } from "@/utils/notify";
import {
NOTIFY_QR_INITIALIZATION_ERROR,
NOTIFY_QR_CAMERA_IN_USE,
NOTIFY_QR_CAMERA_ACCESS_REQUIRED,
NOTIFY_QR_NO_CAMERA,
NOTIFY_QR_HTTPS_REQUIRED,
NOTIFY_QR_CONTACT_EXISTS,
NOTIFY_QR_CONTACT_ERROR,
NOTIFY_QR_REGISTRATION_SUBMITTED,
NOTIFY_QR_REGISTRATION_ERROR,
NOTIFY_QR_URL_COPIED,
NOTIFY_QR_CODE_HELP,
NOTIFY_QR_DID_COPIED,
NOTIFY_QR_INVALID_QR_CODE,
NOTIFY_QR_INVALID_CONTACT_INFO,
NOTIFY_QR_MISSING_DID,
NOTIFY_QR_UNKNOWN_CONTACT_TYPE,
NOTIFY_QR_PROCESSING_ERROR,
createQRContactAddedMessage,
createQRRegistrationSuccessMessage,
QR_TIMEOUT_SHORT,
QR_TIMEOUT_MEDIUM,
QR_TIMEOUT_STANDARD,
QR_TIMEOUT_LONG,
} from "@/constants/notifications";
interface QRScanResult {
rawValue?: string;
barcode?: string;
}
interface IUserNameDialog {
open: (callback: (name: string) => void) => void;
}
@Component({
components: {
QRCodeVue3,
QuickNav,
UserNameDialog,
QrcodeStream,
},
mixins: [PlatformServiceMixin],
})
export default class ContactQRScanShow extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
// Notification helper system
private notify = createNotifyHelpers(this.$notify);
activeDid = "";
apiServer = "";
// Axios instance for API calls
get axios() {
return (this as any).$platformService.axios;
}
givenName = "";
hideRegisterPromptOnNewContact = false;
isRegistered = false;
qrValue = "";
isScanning = false;
profileImageUrl = "";
error: string | null = null;
// QR Scanner properties
isInitializing = true;
initializationStatus = "Initializing camera...";
useQRReader = __USE_QR_READER__;
preferredCamera: "user" | "environment" = "environment";
cameraState: CameraState = "off";
cameraStateMessage?: string;
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
// Add new properties to track scanning state
private lastScannedValue: string = "";
private lastScanTime: number = 0;
private readonly SCAN_DEBOUNCE_MS = 2000; // Prevent duplicate scans within 2 seconds
// Add cleanup tracking
private isCleaningUp = false;
private isMounted = false;
// Add property to track if we're on desktop
private isDesktop = false;
private isFrontCamera = false;
// Computed properties for template classes
get nameWarningClasses(): string {
return "bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 my-4";
}
get setNameButtonClasses(): string {
return "inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md";
}
get qrCodeContainerClasses(): string {
return "block w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto my-4";
}
get scannerContainerClasses(): string {
return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
}
get statusMessageClasses(): string {
return "absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10";
}
get cameraStatusIndicatorClasses(): Record<string, boolean> {
return {
"inline-block w-2 h-2 rounded-full": true,
"bg-green-500": this.cameraState === "ready",
"bg-yellow-500": this.cameraState === "in_use",
"bg-red-500":
this.cameraState === "error" ||
this.cameraState === "permission_denied" ||
this.cameraState === "not_found",
"bg-blue-500": this.cameraState === "off",
};
}
async created() {
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings.isRegistered;
this.profileImageUrl = settings.profileImageUrl || "";
const account = await libsUtil.retrieveAccountMetadata(this.activeDid);
if (account) {
const name =
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : "");
const publicKeyBase64 = Buffer.from(
account.publicKeyHex,
"hex",
).toString("base64");
this.qrValue =
CONTACT_CSV_HEADER +
"\n" +
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
}
} catch (error) {
logger.error("Error initializing component:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error(NOTIFY_QR_INITIALIZATION_ERROR.message);
}
}
async handleBack(): Promise<void> {
await this.cleanupScanner();
this.$router.back();
}
async startScanning() {
if (this.isCleaningUp) {
logger.debug("Cannot start scanning during cleanup");
return;
}
try {
this.error = null;
this.isScanning = true;
this.lastScannedValue = "";
this.lastScanTime = 0;
const scanner = QRScannerFactory.getInstance();
// Add camera state listener
scanner.addCameraStateListener({
onStateChange: (state, message) => {
this.cameraState = state;
this.cameraStateMessage = message;
// Update UI based on camera state
switch (state) {
case "in_use":
this.error = "Camera is in use by another application";
this.isScanning = false;
this.notify.warning(
NOTIFY_QR_CAMERA_IN_USE.message,
QR_TIMEOUT_LONG,
);
break;
case "permission_denied":
this.error = "Camera permission denied";
this.isScanning = false;
this.notify.warning(
NOTIFY_QR_CAMERA_ACCESS_REQUIRED.message,
QR_TIMEOUT_LONG,
);
break;
case "not_found":
this.error = "No camera found";
this.isScanning = false;
this.notify.warning(NOTIFY_QR_NO_CAMERA.message, QR_TIMEOUT_LONG);
break;
case "error":
this.error = this.cameraStateMessage || "Camera error";
this.isScanning = false;
break;
}
},
});
// Check if scanning is supported first
if (!(await scanner.isSupported())) {
this.error =
"Camera access requires HTTPS. Please use a secure connection.";
this.isScanning = false;
this.notify.warning(NOTIFY_QR_HTTPS_REQUIRED.message, QR_TIMEOUT_LONG);
return;
}
// Start scanning
await scanner.startScan();
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
this.isScanning = false;
logger.error("Error starting scan:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
}
}
async stopScanning() {
try {
const scanner = QRScannerFactory.getInstance();
await scanner.stopScan();
} catch (error) {
logger.error("Error stopping scan:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
} finally {
this.isScanning = false;
this.lastScannedValue = "";
this.lastScanTime = 0;
}
}
async cleanupScanner() {
if (this.isCleaningUp) {
return;
}
this.isCleaningUp = true;
try {
logger.info("Cleaning up QR scanner resources");
await this.stopScanning();
await QRScannerFactory.cleanup();
} catch (error) {
logger.error("Error during scanner cleanup:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
} finally {
this.isCleaningUp = false;
}
}
/**
* Handle QR code scan result with debouncing to prevent duplicate scans
*/
async onScanDetect(result: string | QRScanResult) {
try {
// Extract raw value from different possible formats
const rawValue =
typeof result === "string"
? result
: result?.rawValue || result?.barcode;
if (!rawValue) {
logger.warn("Invalid scan result - no value found:", result);
return;
}
// Debounce duplicate scans
const now = Date.now();
if (
rawValue === this.lastScannedValue &&
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
) {
logger.info("Ignoring duplicate scan:", rawValue);
return;
}
// Update scan tracking
this.lastScannedValue = rawValue;
this.lastScanTime = now;
logger.info("Processing QR code scan result:", rawValue);
let contact: Contact;
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
const jwt = getContactJwtFromJwtUrl(rawValue);
if (!jwt) {
logger.warn("Invalid QR code format - no JWT found in URL");
this.notify.error(NOTIFY_QR_INVALID_QR_CODE.message);
return;
}
logger.info("Decoding JWT payload from QR code");
const decodedJwt = await decodeEndorserJwt(jwt);
// Process JWT and contact info
if (!decodedJwt?.payload?.own) {
logger.warn("Invalid JWT payload - missing 'own' field");
this.notify.error(NOTIFY_QR_INVALID_CONTACT_INFO.message);
return;
}
const contactInfo = decodedJwt.payload.own;
const did = contactInfo.did || decodedJwt.payload.iss;
if (!did) {
logger.warn("Invalid contact info - missing DID");
this.notify.error(NOTIFY_QR_MISSING_DID.message);
return;
}
// Create contact object
contact = {
did: did,
name: contactInfo.name || "",
publicKeyBase64: contactInfo.publicKeyBase64 || "",
seesMe: contactInfo.seesMe || false,
registered: contactInfo.registered || false,
};
} else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
const lines = rawValue.split(/\n/);
contact = libsUtil.csvLineToContact(lines[1]);
} else {
this.notify.error(NOTIFY_QR_UNKNOWN_CONTACT_TYPE.message);
return;
}
// Add contact but keep scanning
logger.info("Adding new contact to database:", {
did: contact.did,
name: contact.name,
});
await this.addNewContact(contact);
} catch (error) {
logger.error("Error processing contact QR code:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error(
error instanceof Error
? error.message
: NOTIFY_QR_PROCESSING_ERROR.message,
);
}
}
async setVisibility(contact: Contact, visibility: boolean) {
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
contact,
visibility,
);
if (result.error) {
this.notify.error(result.error as string, QR_TIMEOUT_LONG);
} else if (!result.success) {
logger.warn("Unexpected result from setting visibility:", result);
}
}
async register(contact: Contact) {
logger.info("Submitting contact registration", {
did: contact.did,
name: contact.name,
});
this.notify.toast(
NOTIFY_QR_REGISTRATION_SUBMITTED.message,
QR_TIMEOUT_SHORT,
);
try {
const regResult = await register(
this.activeDid,
this.apiServer,
this.axios,
contact,
);
if (regResult.success) {
contact.registered = true;
await this.$updateContact(contact.did, { registered: true });
logger.info("Contact registration successful", { did: contact.did });
this.notify.success(
createQRRegistrationSuccessMessage(contact.name || ""),
QR_TIMEOUT_LONG,
);
} else {
this.notify.error(
(regResult.error as string) || NOTIFY_QR_REGISTRATION_ERROR.message,
QR_TIMEOUT_LONG,
);
}
} catch (error) {
logger.error("Error registering contact:", {
did: contact.did,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
let userMessage = "There was an error.";
const serverError = error as AxiosError;
if (serverError) {
if (
serverError.response?.data &&
typeof serverError.response.data === "object" &&
"message" in serverError.response.data
) {
userMessage = (serverError.response.data as { message: string })
.message;
} else if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.notify.error(userMessage, QR_TIMEOUT_LONG);
}
}
onScanError(error: Error) {
this.error = error.message;
logger.error("QR code scan error:", {
error: error.message,
stack: error.stack,
});
}
async onCopyUrlToClipboard() {
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
useClipboard()
.copy(jwtUrl)
.then(() => {
this.notify.toast(NOTIFY_QR_URL_COPIED.message, QR_TIMEOUT_MEDIUM);
});
}
toastQRCodeHelp() {
this.notify.info(NOTIFY_QR_CODE_HELP.message, QR_TIMEOUT_LONG);
}
onCopyDidToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
useClipboard()
.copy(this.activeDid)
.then(() => {
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
});
}
openUserNameDialog() {
(this.$refs.userNameDialog as IUserNameDialog).open((name: string) => {
this.givenName = name;
});
}
// Lifecycle hooks
mounted() {
this.isMounted = true;
this.isDesktop = this.detectDesktopBrowser();
document.addEventListener("pause", this.handleAppPause);
document.addEventListener("resume", this.handleAppResume);
// Start scanning automatically when view is loaded
this.startScanning();
// Apply mirroring after a short delay to ensure video element is ready
setTimeout(() => {
const videoElement = document.querySelector(
".qr-scanner video",
) as HTMLVideoElement;
if (videoElement) {
videoElement.style.transform = "scaleX(-1)";
}
}, 1000);
}
beforeDestroy() {
this.isMounted = false;
document.removeEventListener("pause", this.handleAppPause);
document.removeEventListener("resume", this.handleAppResume);
this.cleanupScanner();
}
async handleAppPause() {
if (!this.isMounted) return;
logger.info("App paused, stopping scanner");
await this.stopScanning();
}
handleAppResume() {
if (!this.isMounted) return;
logger.info("App resumed, scanner can be restarted by user");
this.isScanning = false;
}
async addNewContact(contact: Contact) {
try {
logger.info("Opening database connection for new contact");
// Check if contact already exists
const existingContact = await this.$getContact(contact.did);
if (existingContact) {
logger.info("Contact already exists", { did: contact.did });
this.notify.warning(NOTIFY_QR_CONTACT_EXISTS.message, QR_TIMEOUT_LONG);
return;
}
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(
(this as any)._parseJsonField(contact.contactMethods, []),
);
await this.$insertContact(contact);
if (this.activeDid) {
logger.info("Setting contact visibility", { did: contact.did });
await this.setVisibility(contact, true);
contact.seesMe = true;
}
this.notify.success(
createQRContactAddedMessage(!!this.activeDid),
QR_TIMEOUT_STANDARD,
);
if (
this.isRegistered &&
!this.hideRegisterPromptOnNewContact &&
!contact.registered
) {
setTimeout(() => {
this.notify.confirm(
"Register",
"Do you want to register them?",
{
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$updateSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$updateSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onYes: async () => {
await this.register(contact);
},
promptToStopAsking: true,
},
-1,
);
}, 500);
}
} catch (error) {
logger.error("Error saving contact to database:", {
did: contact.did,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error(NOTIFY_QR_CONTACT_ERROR.message, QR_TIMEOUT_LONG);
}
}
async onInit(promise: Promise<void>): Promise<void> {
logger.log("[QRScanner] onInit called");
try {
await promise;
this.isInitializing = false;
this.cameraState = "ready";
} catch (error) {
const wrappedError =
error instanceof Error ? error : new Error(String(error));
this.error = wrappedError.message;
this.cameraState = "error";
this.isInitializing = false;
logger.error("Error during QR scanner initialization:", {
error: wrappedError.message,
stack: wrappedError.stack,
});
}
}
onCameraOn(): void {
this.cameraState = "active";
this.isInitializing = false;
this.isFrontCamera = this.preferredCamera === "user";
this.applyCameraMirroring();
}
onCameraOff(): void {
this.cameraState = "off";
}
onDetect(result: unknown): void {
this.isScanning = true;
this.cameraState = "active";
try {
let rawValue: string | undefined;
if (
Array.isArray(result) &&
result.length > 0 &&
"rawValue" in result[0]
) {
rawValue = result[0].rawValue;
} else if (result && typeof result === "object" && "rawValue" in result) {
rawValue = (result as { rawValue: string }).rawValue;
}
if (rawValue) {
this.isInitializing = false;
this.initializationStatus = "QR code captured!";
this.onScanDetect(rawValue);
}
} catch (error) {
this.handleError(error);
} finally {
this.cameraState = "active";
}
}
onDecode(result: string): void {
try {
this.isInitializing = false;
this.initializationStatus = "QR code captured!";
this.onScanDetect(result);
} catch (error) {
this.handleError(error);
}
}
toggleCamera(): void {
this.preferredCamera =
this.preferredCamera === "user" ? "environment" : "user";
this.isFrontCamera = this.preferredCamera === "user";
this.applyCameraMirroring();
}
private handleError(error: unknown): void {
const wrappedError =
error instanceof Error ? error : new Error(String(error));
this.error = wrappedError.message;
this.cameraState = "error";
}
onError(error: Error): void {
this.error = error.message;
this.cameraState = "error";
logger.error("QR code scan error:", {
error: error.message,
stack: error.stack,
});
}
// Add method to detect desktop browser
private detectDesktopBrowser(): boolean {
const userAgent = navigator.userAgent.toLowerCase();
return !/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent,
);
}
// Add method to apply camera mirroring
private applyCameraMirroring(): void {
const videoElement = document.querySelector(
".qr-scanner video",
) as HTMLVideoElement;
if (videoElement) {
// Mirror if it's desktop or front camera on mobile
const shouldMirror =
this.isDesktop || (this.isFrontCamera && !this.isDesktop);
videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none";
}
}
}
</script>
<style scoped>
.aspect-square {
aspect-ratio: 1 / 1;
}
/* Update styles for camera mirroring */
:deep(.qr-scanner) {
position: relative;
}
/* Remove the default mirroring from CSS since we're handling it in JavaScript */
:deep(.qr-scanner video) {
transform: none;
}
/* Ensure the canvas for QR detection is not mirrored */
:deep(.qr-scanner canvas) {
transform: none;
}
</style>