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.
934 lines
27 KiB
934 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>
|
|
|
|
<p
|
|
v-if="!givenName"
|
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 my-4"
|
|
>
|
|
<span class="text-red">Beware!</span>
|
|
You aren't sharing your name, so quickly
|
|
<br />
|
|
<span
|
|
class="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-1.5 py-1 rounded-md"
|
|
@click="openUserNameDialog"
|
|
>
|
|
click here to set it for them.
|
|
</span>
|
|
</p>
|
|
|
|
<UserNameDialog ref="userNameDialog" />
|
|
|
|
<div
|
|
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
|
|
class="block w-[90vw] max-w-[40vh] mx-auto my-4 border border-slate-500"
|
|
@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="640"
|
|
:height="640"
|
|
: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 that first, these contacts won't see your activity.
|
|
</div>
|
|
|
|
<div class="text-center my-6">
|
|
<div
|
|
v-if="isScanning && !isNativePlatform"
|
|
class="relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[40vh] mx-auto"
|
|
>
|
|
<!-- Status Message -->
|
|
<div
|
|
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10"
|
|
>
|
|
<div
|
|
v-if="isInitializing"
|
|
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>{{ initializationStatus }}</span>
|
|
</div>
|
|
<p
|
|
v-else-if="isScanning"
|
|
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="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
|
|
<span>Ready to scan</span>
|
|
</p>
|
|
</div>
|
|
|
|
<qrcode-stream
|
|
v-if="useQRReader && !isNativePlatform"
|
|
:camera="preferredCamera"
|
|
@decode="onDecode"
|
|
@init="onInit"
|
|
@detect="onDetect"
|
|
@error="onError"
|
|
@camera-on="onCameraOn"
|
|
@camera-off="onCameraOff"
|
|
/>
|
|
|
|
<!-- Scanning Frame -->
|
|
<div
|
|
class="absolute inset-0 border-2"
|
|
:class="{
|
|
'border-blue-500': !error && !isScanning,
|
|
'border-green-500 animate-pulse': isScanning,
|
|
'border-red-500': error,
|
|
}"
|
|
style="opacity: 0.5; pointer-events: none"
|
|
></div>
|
|
|
|
<!-- Camera Switch Button -->
|
|
<button
|
|
class="absolute bottom-4 left-4 bg-white rounded-full p-2 leading-none shadow-lg"
|
|
title="Switch camera"
|
|
@click="toggleCamera"
|
|
>
|
|
<font-awesome icon="camera-rotate" class="size-6 text-gray-600" />
|
|
</button>
|
|
|
|
<!-- Camera Stop Button -->
|
|
<button
|
|
class="absolute bottom-4 right-4 bg-white rounded-full p-2 leading-none shadow-lg"
|
|
title="Stop camera"
|
|
@click="stopScanning"
|
|
>
|
|
<font-awesome icon="xmark" class="size-6 text-gray-600" />
|
|
</button>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="flex items-center justify-center aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[40vh] mx-auto">
|
|
<button
|
|
v-if="isNativePlatform"
|
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white text-lg px-3 py-2 rounded-lg"
|
|
@click="$router.push({ name: 'contact-qr-scan' })"
|
|
>
|
|
Scan QR Code
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white text-lg px-3 py-2 rounded-lg"
|
|
@click="startScanning"
|
|
>
|
|
Scan QR Code
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError } from "axios";
|
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { useClipboard } from "@vueuse/core";
|
|
import { Capacitor } from "@capacitor/core";
|
|
import { QrcodeStream } from "vue-qrcode-reader";
|
|
import type { RouteLocationNormalized, NavigationGuardNext } from "vue-router";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
|
import { NotificationIface } from "../constants/app";
|
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
|
import {
|
|
generateEndorserJwtUrlForAccount,
|
|
register,
|
|
setVisibilityUtil,
|
|
} from "../libs/endorserServer";
|
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
|
import { retrieveAccountMetadata } from "../libs/util";
|
|
import { Router } from "vue-router";
|
|
import { logger } from "../utils/logger";
|
|
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
|
|
|
interface QRScanResult {
|
|
rawValue?: string;
|
|
barcode?: string;
|
|
}
|
|
|
|
interface IUserNameDialog {
|
|
open: (callback: (name: string) => void) => void;
|
|
}
|
|
|
|
@Component({
|
|
components: {
|
|
QRCodeVue3,
|
|
QuickNav,
|
|
UserNameDialog,
|
|
QrcodeStream,
|
|
},
|
|
})
|
|
export default class ContactQRScanShow extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$router!: Router;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
givenName = "";
|
|
hideRegisterPromptOnNewContact = false;
|
|
isRegistered = false;
|
|
qrValue = "";
|
|
isScanning = false;
|
|
error: string | null = null;
|
|
isNativePlatform = Capacitor.isNativePlatform();
|
|
|
|
// QR Scanner properties
|
|
isInitializing = true;
|
|
initializationStatus = "Initializing camera...";
|
|
useQRReader = __USE_QR_READER__;
|
|
preferredCamera: "user" | "environment" = "environment";
|
|
cameraStatus = "Initializing";
|
|
|
|
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;
|
|
|
|
async created() {
|
|
try {
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.givenName = settings.firstName || "";
|
|
this.hideRegisterPromptOnNewContact =
|
|
!!settings.hideRegisterPromptOnNewContact;
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
const account = await retrieveAccountMetadata(this.activeDid);
|
|
if (account) {
|
|
const name =
|
|
(settings.firstName || "") +
|
|
(settings.lastName ? ` ${settings.lastName}` : "");
|
|
this.qrValue = await generateEndorserJwtUrlForAccount(
|
|
account,
|
|
!!settings.isRegistered,
|
|
name,
|
|
settings.profileImageUrl || "",
|
|
false,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.error("Error initializing component:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Initialization Error",
|
|
text: "Failed to initialize QR scanner. Please try again.",
|
|
});
|
|
}
|
|
}
|
|
|
|
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.isInitializing = true;
|
|
this.initializationStatus = "Initializing camera...";
|
|
this.lastScannedValue = "";
|
|
this.lastScanTime = 0;
|
|
|
|
const scanner = QRScannerFactory.getInstance();
|
|
|
|
// 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.isInitializing = false;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "HTTPS Required",
|
|
text: "Camera access requires a secure (HTTPS) connection",
|
|
},
|
|
5000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check permissions first
|
|
if (!(await scanner.checkPermissions())) {
|
|
this.initializationStatus = "Requesting camera permission...";
|
|
const granted = await scanner.requestPermissions();
|
|
if (!granted) {
|
|
this.error = "Camera permission denied";
|
|
this.isScanning = false;
|
|
this.isInitializing = false;
|
|
// Show notification for better visibility
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Camera Access Required",
|
|
text: "Camera permission denied",
|
|
},
|
|
5000,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// For native platforms, use the scanner service
|
|
scanner.addListener({
|
|
onScan: this.onScanDetect,
|
|
onError: this.onScanError,
|
|
});
|
|
|
|
// Start scanning
|
|
await scanner.startScan();
|
|
} catch (error) {
|
|
this.error = error instanceof Error ? error.message : String(error);
|
|
this.isScanning = false;
|
|
this.isInitializing = 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;
|
|
}
|
|
}
|
|
|
|
danger(message: string, title: string = "Error", timeout = 5000) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: title,
|
|
text: message,
|
|
},
|
|
timeout,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Extract JWT
|
|
const jwt = getContactJwtFromJwtUrl(rawValue);
|
|
if (!jwt) {
|
|
logger.warn("Invalid QR code format - no JWT found in URL");
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Invalid QR Code",
|
|
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Process JWT and contact info
|
|
logger.info("Decoding JWT payload from QR code");
|
|
const decodedJwt = await decodeEndorserJwt(jwt);
|
|
if (!decodedJwt?.payload?.own) {
|
|
logger.warn("Invalid JWT payload - missing 'own' field");
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Invalid Contact Info",
|
|
text: "The contact information is incomplete or invalid.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const contactInfo = decodedJwt.payload.own;
|
|
if (!contactInfo.did) {
|
|
logger.warn("Invalid contact info - missing DID");
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Invalid Contact",
|
|
text: "The contact DID is missing.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Create contact object
|
|
const contact = {
|
|
did: contactInfo.did,
|
|
name: contactInfo.name || "",
|
|
email: contactInfo.email || "",
|
|
phone: contactInfo.phone || "",
|
|
company: contactInfo.company || "",
|
|
title: contactInfo.title || "",
|
|
notes: contactInfo.notes || "",
|
|
};
|
|
|
|
// Add contact and stop scanning
|
|
logger.info("Adding new contact to database:", {
|
|
did: contact.did,
|
|
name: contact.name,
|
|
});
|
|
await this.addNewContact(contact);
|
|
await this.stopScanning();
|
|
} 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({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Could not process QR code. Please try again.",
|
|
});
|
|
}
|
|
}
|
|
|
|
async setVisibility(contact: Contact, visibility: boolean) {
|
|
const result = await setVisibilityUtil(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
db,
|
|
contact,
|
|
visibility,
|
|
);
|
|
if (result.error) {
|
|
this.danger(result.error as string, "Error Setting Visibility");
|
|
} 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(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
text: "",
|
|
title: "Registration submitted...",
|
|
},
|
|
1000,
|
|
);
|
|
|
|
try {
|
|
const regResult = await register(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
);
|
|
if (regResult.success) {
|
|
contact.registered = true;
|
|
db.contacts.update(contact.did, { registered: true });
|
|
logger.info("Contact registration successful", { did: contact.did });
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Registration Success",
|
|
text:
|
|
(contact.name || "That unnamed person") + " has been registered.",
|
|
},
|
|
5000,
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Registration Error",
|
|
text:
|
|
(regResult.error as string) ||
|
|
"Something went wrong during registration.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} 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(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Registration Error",
|
|
text: userMessage,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
onScanError(error: Error) {
|
|
this.error = error.message;
|
|
logger.error("QR code scan error:", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
}
|
|
|
|
onCopyUrlToClipboard() {
|
|
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
|
useClipboard()
|
|
.copy(this.qrValue)
|
|
.then(() => {
|
|
// console.log("Contact URL:", this.qrValue);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
title: "Copied",
|
|
text: "Contact URL was copied to clipboard.",
|
|
},
|
|
2000,
|
|
);
|
|
});
|
|
}
|
|
|
|
toastQRCodeHelp() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
text: "Click the QR code to copy your contact info to your clipboard.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
onCopyDidToClipboard() {
|
|
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
|
useClipboard()
|
|
.copy(this.activeDid)
|
|
.then(() => {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Copied",
|
|
text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
|
|
},
|
|
5000,
|
|
);
|
|
});
|
|
}
|
|
|
|
openUserNameDialog() {
|
|
(this.$refs.userNameDialog as IUserNameDialog).open((name: string) => {
|
|
this.givenName = name;
|
|
});
|
|
}
|
|
|
|
// Lifecycle hooks
|
|
mounted() {
|
|
this.isMounted = true;
|
|
document.addEventListener("pause", this.handleAppPause);
|
|
document.addEventListener("resume", this.handleAppResume);
|
|
// Start scanning automatically when view is loaded, but only on web platform
|
|
if (!this.isNativePlatform) {
|
|
this.startScanning();
|
|
}
|
|
}
|
|
|
|
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");
|
|
await db.open();
|
|
|
|
// Check if contact already exists
|
|
const existingContacts = await db.contacts.toArray();
|
|
const existingContact = existingContacts.find(
|
|
(c) => c.did === contact.did,
|
|
);
|
|
|
|
if (existingContact) {
|
|
logger.info("Contact already exists", { did: contact.did });
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Contact Exists",
|
|
text: "This contact has already been added to your list.",
|
|
},
|
|
3000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Add new contact
|
|
await db.contacts.add(contact);
|
|
|
|
if (this.activeDid) {
|
|
logger.info("Setting contact visibility", { did: contact.did });
|
|
await this.setVisibility(contact, true);
|
|
contact.seesMe = true;
|
|
}
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Contact Added",
|
|
text: this.activeDid
|
|
? "They were added, and your activity is visible to them."
|
|
: "They were added.",
|
|
},
|
|
3000,
|
|
);
|
|
|
|
if (
|
|
this.isRegistered &&
|
|
!this.hideRegisterPromptOnNewContact &&
|
|
!contact.registered
|
|
) {
|
|
setTimeout(() => {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Register",
|
|
text: "Do you want to register them?",
|
|
onCancel: async (stopAsking?: boolean) => {
|
|
if (stopAsking) {
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
hideRegisterPromptOnNewContact: stopAsking,
|
|
});
|
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
}
|
|
},
|
|
onNo: async (stopAsking?: boolean) => {
|
|
if (stopAsking) {
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
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(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Contact Error",
|
|
text: "Could not save contact. Check if it already exists.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
async onInit(promise: Promise<void>): Promise<void> {
|
|
logger.log("[QRScanner] onInit called");
|
|
if (this.isNativePlatform) {
|
|
logger.log("Skipping QR scanner initialization on native platform");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await promise;
|
|
this.isInitializing = false;
|
|
this.cameraStatus = "Ready";
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
this.error = wrappedError.message;
|
|
this.cameraStatus = "Error";
|
|
this.isInitializing = false;
|
|
logger.error("Error during QR scanner initialization:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
});
|
|
}
|
|
}
|
|
|
|
onCameraOn(): void {
|
|
this.cameraStatus = "Active";
|
|
this.isInitializing = false;
|
|
}
|
|
|
|
onCameraOff(): void {
|
|
this.cameraStatus = "Off";
|
|
}
|
|
|
|
onDetect(result: unknown): void {
|
|
this.isScanning = true;
|
|
this.cameraStatus = "Detecting";
|
|
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.rawValue;
|
|
}
|
|
if (rawValue) {
|
|
this.isInitializing = false;
|
|
this.initializationStatus = "QR code captured!";
|
|
this.onScanDetect(rawValue);
|
|
}
|
|
} catch (error) {
|
|
this.handleError(error);
|
|
} finally {
|
|
this.isScanning = false;
|
|
this.cameraStatus = "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";
|
|
}
|
|
|
|
private handleError(error: unknown): void {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
this.error = wrappedError.message;
|
|
this.cameraStatus = "Error";
|
|
}
|
|
|
|
onError(error: Error): void {
|
|
this.error = error.message;
|
|
this.cameraStatus = "Error";
|
|
logger.error("QR code scan error:", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aspect-square {
|
|
aspect-ratio: 1 / 1;
|
|
}
|
|
</style>
|
|
|