forked from trent_larson/crowd-funder-for-time-pwa
Dynamic padding to clear certain iOS UI elements such as the notch, dynamic island and gesture bar, to ensure they don't overlap with our own UI elements.
935 lines
27 KiB
Vue
935 lines
27 KiB
Vue
<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 mt-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>
|