Browse Source

refactor(ContactQRScanShowView): simplify QR scanning and contact processing

- Remove complex camera lifecycle management code
- Streamline QR code processing and contact handling
- Simplify clipboard operations using native API
- Remove redundant error handling and notification methods
- Consolidate contact processing logic into focused methods
qrcode-capacitor
Matthew Raymer 2 months ago
parent
commit
62553a37aa
  1. 317
      src/components/QRScannerDialog.vue
  2. 812
      src/views/ContactQRScanShowView.vue

317
src/components/QRScannerDialog.vue

@ -0,0 +1,317 @@
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
>
<span v-if="state.isProcessing">{{ state.processingStatus }}</span>
<span v-else>Scan QR Code</span>
</div>
<div
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white cursor-pointer"
@click="close()"
>
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</div>
<div class="mt-8">
<!-- Web QR Code Scanner -->
<qrcode-stream
v-if="useQRReader"
class="w-full max-w-lg mx-auto"
@detect="onScanDetect"
@error="onScanError"
/>
<!-- Mobile Camera Button -->
<div v-else class="text-center mt-4">
<button
v-if="!state.isProcessing"
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-4 py-2 rounded-md"
@click="openMobileCamera"
>
Open Camera
</button>
<div v-else class="text-center">
<font-awesome icon="spinner" class="fa-spin fa-3x" />
<p class="mt-2">{{ state.processingDetails }}</p>
</div>
</div>
<p v-if="state.error" class="mt-4 text-red-500 text-center">
{{ state.error }}
</p>
<p class="mt-4 text-sm text-gray-600 text-center">
Position the QR code within the camera view to scan
</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader";
import { reactive } from "vue";
import {
BarcodeScanner,
type ScanResult,
} from "@capacitor-mlkit/barcode-scanning";
import type { PluginListenerHandle } from "@capacitor/core";
import { logger } from "../utils/logger";
import { NotificationIface } from "../constants/app";
// Declare global constants
declare const __USE_QR_READER__: boolean;
declare const __IS_MOBILE__: boolean;
interface AppState {
isProcessing: boolean;
processingStatus: string;
processingDetails: string;
error: string;
}
@Component({
components: {
QrcodeStream,
},
})
export default class QRScannerDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
visible = false;
private scanListener: PluginListenerHandle | null = null;
private onScanCallback: ((result: string) => void) | null = null;
state = reactive<AppState>({
isProcessing: false,
processingStatus: "",
processingDetails: "",
error: "",
});
async open(onScan: (result: string) => void) {
this.onScanCallback = onScan;
this.visible = true;
this.state.error = "";
if (!this.useQRReader) {
// Check if barcode scanning is supported on mobile
try {
const { supported } = await BarcodeScanner.isSupported();
if (!supported) {
this.showError("Barcode scanning is not supported on this device");
return;
}
} catch (error) {
this.showError("Failed to check barcode scanner support");
return;
}
}
}
close() {
this.visible = false;
this.stopScanning().catch((error) => {
logger.error("Error stopping scanner during close:", error);
});
this.onScanCallback = null;
}
async openMobileCamera() {
try {
this.state.isProcessing = true;
this.state.processingStatus = "Starting camera...";
logger.log("Opening mobile camera - starting initialization");
// Check current permission status
const status = await BarcodeScanner.checkPermissions();
logger.log("Camera permission status:", JSON.stringify(status, null, 2));
if (status.camera !== "granted") {
// Request permission if not granted
logger.log("Requesting camera permissions...");
const permissionStatus = await BarcodeScanner.requestPermissions();
if (permissionStatus.camera !== "granted") {
throw new Error("Camera permission not granted");
}
logger.log(
"Camera permission granted:",
JSON.stringify(permissionStatus, null, 2),
);
}
// Remove any existing listener first
await this.cleanupScanListener();
// Set up the listener before starting the scan
logger.log("Setting up new barcode listener");
this.scanListener = await BarcodeScanner.addListener(
"barcodesScanned",
async (result: ScanResult) => {
logger.log(
"Barcode scan result received:",
JSON.stringify(result, null, 2),
);
if (result.barcodes && result.barcodes.length > 0) {
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
await this.handleScanResult(result.barcodes[0].rawValue);
}
},
);
logger.log("Barcode listener setup complete");
// Start the scanner
logger.log("Starting barcode scanner");
await BarcodeScanner.startScan();
logger.log("Barcode scanner started successfully");
this.state.isProcessing = false;
this.state.processingStatus = "";
} catch (error) {
logger.error("Failed to open camera:", error);
this.state.isProcessing = false;
this.state.processingStatus = "";
this.showError(
error instanceof Error ? error.message : "Failed to open camera",
);
// Cleanup on error
await this.cleanupScanListener();
}
}
private async handleScanResult(rawValue: string) {
try {
this.state.isProcessing = true;
this.state.processingStatus = "Processing QR code...";
this.state.processingDetails = `Scanned value: ${rawValue}`;
// Stop scanning before processing
await this.stopScanning();
if (this.onScanCallback) {
await this.onScanCallback(rawValue);
// Only close after the callback is complete
this.close();
}
} catch (error) {
logger.error("Error handling scan result:", error);
this.showError("Failed to process scan result");
} finally {
this.state.isProcessing = false;
this.state.processingStatus = "";
this.state.processingDetails = "";
}
}
private async cleanupScanListener() {
try {
if (this.scanListener) {
await this.scanListener.remove();
this.scanListener = null;
}
} catch (error) {
logger.error("Error removing scan listener:", error);
}
}
async stopScanning() {
try {
await this.cleanupScanListener();
if (!this.useQRReader) {
// Stop the native scanner
await BarcodeScanner.stopScan();
}
} catch (error) {
logger.error("Error stopping scanner:", error);
throw error;
}
}
// Web QR reader handlers
async onScanDetect(result: { rawValue: string }) {
await this.handleScanResult(result.rawValue);
}
onScanError(error: Error) {
logger.error("Scan error:", error);
this.showError("Failed to scan QR code");
}
private showError(message: string) {
this.state.error = message;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
5000,
);
}
get useQRReader(): boolean {
return __USE_QR_READER__;
}
get isMobile(): boolean {
return __IS_MOBILE__;
}
}
</script>
<style>
.dialog-overlay {
z-index: 60;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 700px;
position: relative;
z-index: 61;
}
/* Add styles for the camera preview */
.qrcode-stream {
position: relative;
z-index: 62;
}
.qrcode-stream video {
width: 100%;
height: auto;
max-height: 70vh;
object-fit: contain;
}
/* Ensure mobile camera elements are also properly layered */
.barcode-scanner-container {
position: relative;
z-index: 62;
width: 100%;
height: 100%;
}
</style>

812
src/views/ContactQRScanShowView.vue

@ -91,35 +91,21 @@
<div class="text-center">
<h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
<!-- Web QR Code Scanner -->
<qrcode-stream
v-if="useQRReader"
@detect="onScanDetect"
@error="onScanError"
/>
<!-- Mobile Camera Button -->
<div v-else class="mt-4">
<button
class="bg-blue-500 text-white px-4 py-2 rounded-md"
@click="openMobileCamera"
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-4 py-2 rounded-md mt-4"
@click="openQRScanner"
>
Open Camera
Scan QR Code
</button>
<p class="mt-2 text-sm text-gray-600">
If you do not see the camera, check your camera permissions.
</p>
</div>
</div>
</section>
<QRScannerDialog ref="qrScannerDialog" />
</template>
<script lang="ts">
import { AxiosError } from "axios";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { PlatformService } from "../services/PlatformService";
import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import { NotificationIface } from "../constants/app";
@ -127,65 +113,17 @@ import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { isDid, register, setVisibilityUtil } from "../libs/endorserServer";
import { isDid, register } from "../libs/endorserServer";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { Router } from "vue-router";
import { logger } from "../utils/logger";
import { App } from "@capacitor/app";
import type { PluginListenerHandle } from "@capacitor/core";
import {
BarcodeScanner,
type ScanResult,
} from "@capacitor-mlkit/barcode-scanning";
import { reactive } from "vue";
import QRScannerDialog from "../components/QRScannerDialog.vue";
// Declare global constants
declare const __USE_QR_READER__: boolean;
declare const __IS_MOBILE__: boolean;
// Define all possible camera states
type CameraState =
| "initializing"
| "ready"
| "error"
| "checking_permissions"
| "no_camera_capability"
| "permission_status_checked"
| "requesting_permission"
| "permission_requested"
| "permission_check_error"
| "camera_initialized"
| "camera_error"
| "opening_camera"
| "capture_already_in_progress"
| "photo_captured"
| "processing_photo"
| "no_image_data"
| "capture_error"
| "user_cancelled"
| "permission_error"
| "hardware_unavailable"
| "capture_completed"
| "cleanup"
| "processing_completed";
// Define all possible QR processing states
type QRProcessingState =
| "processing_image"
| "qr_code_detected"
| "no_qr_code_found"
| "processing_error";
interface CameraStateHistoryEntry {
state: CameraState | QRProcessingState;
timestamp: number;
details?: Record<string, unknown>;
}
interface AppStateChangeEvent {
isActive: boolean;
}
interface ScannerState {
isSupported: boolean;
granted: boolean;
@ -211,6 +149,7 @@ interface AppState {
QrcodeStream,
QuickNav,
UserNameDialog,
QRScannerDialog,
},
})
export default class ContactQRScanShowView extends Vue {
@ -220,6 +159,7 @@ export default class ContactQRScanShowView extends Vue {
userNameDialog: {
open: (callback: (name: string) => void) => void;
};
qrScannerDialog: QRScannerDialog;
};
activeDid = "";
@ -231,17 +171,6 @@ export default class ContactQRScanShowView extends Vue {
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
private platformService: PlatformService =
PlatformServiceFactory.getInstance();
private cameraActive = false;
private lastCameraState: CameraState | QRProcessingState = "initializing";
private cameraStateHistory: CameraStateHistoryEntry[] = [];
private readonly STATE_HISTORY_LIMIT = 20;
private isCapturingPhoto = false;
private appStateListener?: { remove: () => Promise<void> };
private scanListener: PluginListenerHandle | null = null;
private state = reactive<AppState>({
isProcessing: false,
processingStatus: "",
@ -260,372 +189,74 @@ export default class ContactQRScanShowView extends Vue {
});
async created() {
logger.log("ContactQRScanShow component created");
try {
// Remove any existing listeners first
await this.cleanupAppListeners();
// Add app state listeners
await this.setupAppLifecycleListeners();
// Load initial data
await this.loadInitialData();
// Check if barcode scanning is supported
const { supported } = await BarcodeScanner.isSupported();
if (!supported) {
this.showError("Barcode scanning is not supported on this device");
return;
}
// Initialize scanner state but don't request permissions yet
this.state.scannerState.isSupported = supported;
logger.log("ContactQRScanShow initialization complete");
} catch (error) {
logger.error("Failed to initialize ContactQRScanShow:", error);
this.showError("Failed to initialize. Please try again.");
}
}
private async cleanupAppListeners(): Promise<void> {
try {
if (this.appStateListener) {
await this.appStateListener.remove();
this.appStateListener = undefined;
}
await App.removeAllListeners();
logger.log("App listeners cleaned up successfully");
} catch (error) {
logger.error("Error cleaning up app listeners:", error);
throw error;
}
}
private async setupAppLifecycleListeners(): Promise<void> {
private async loadInitialData() {
try {
// Add app state change listener
this.appStateListener = await App.addListener(
"appStateChange",
(state: AppStateChangeEvent) => {
const stateInfo = {
isActive: state.isActive,
timestamp: new Date().toISOString(),
cameraActive: this.cameraActive,
scannerState: {
...this.state.scannerState,
// Convert complex objects to strings to avoid [object Object]
error: this.state.scannerState.error?.toString() || null,
},
};
logger.log("App state changed:", JSON.stringify(stateInfo, null, 2));
if (!state.isActive) {
this.cleanupCamera();
}
},
);
// Add pause listener
await App.addListener("pause", () => {
const pauseInfo = {
timestamp: new Date().toISOString(),
cameraActive: this.cameraActive,
scannerState: {
...this.state.scannerState,
error: this.state.scannerState.error?.toString() || null,
},
isProcessing: this.state.isProcessing,
};
logger.log("App paused:", JSON.stringify(pauseInfo, null, 2));
this.cleanupCamera();
});
// Add resume listener
await App.addListener("resume", () => {
const resumeInfo = {
timestamp: new Date().toISOString(),
cameraActive: this.cameraActive,
scannerState: {
...this.state.scannerState,
error: this.state.scannerState.error?.toString() || null,
},
isProcessing: this.state.isProcessing,
};
logger.log("App resumed:", JSON.stringify(resumeInfo, null, 2));
});
logger.log("App lifecycle listeners setup complete");
} catch (error) {
logger.error("Error setting up app lifecycle listeners:", error);
throw error;
}
const settings = await db.settings.toCollection().first();
if (!settings) {
throw new Error("No settings found");
}
private async loadInitialData() {
try {
// Load settings from DB
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (settings) {
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.hideRegisterPromptOnNewContact =
settings.hideRegisterPromptOnNewContact || false;
}
logger.log("Initial data loaded successfully");
} catch (error) {
logger.error("Failed to load initial data:", error);
throw error;
}
}
private async cleanupCamera() {
try {
if (this.cameraActive) {
this.cameraActive = false;
this.isCapturingPhoto = false;
this.addCameraState("cleanup");
logger.log("Camera cleaned up successfully");
}
} catch (error) {
logger.error("Error during camera cleanup:", error);
}
}
private addCameraState(
state: CameraState | QRProcessingState,
details?: Record<string, unknown>,
) {
// Prevent duplicate state transitions
if (this.lastCameraState === state) {
return;
}
const entry: CameraStateHistoryEntry = {
state,
timestamp: Date.now(),
details: {
...details,
cameraActive: this.cameraActive,
isCapturingPhoto: this.isCapturingPhoto,
isProcessing: this.state.isProcessing,
},
};
this.cameraStateHistory.push(entry);
this.isRegistered = !!settings.isRegistered;
if (this.cameraStateHistory.length > this.STATE_HISTORY_LIMIT) {
this.cameraStateHistory.shift();
if (this.activeDid.startsWith(ETHR_DID_PREFIX)) {
this.qrValue = `${window.location.origin}/contact/${this.activeDid}`;
}
this.logWithDetails("Camera state transition", {
state: entry.state,
timestamp: entry.timestamp,
details: entry.details,
});
this.lastCameraState = state;
}
beforeDestroy() {
logger.log(
"ContactQRScanShow component being destroyed, initiating cleanup",
);
// Clean up scanner
this.stopScanning().catch((error) => {
logger.error("Error stopping scanner during destroy:", error);
});
// Remove all app lifecycle listeners
const cleanupListeners = async () => {
if (this.appStateListener) {
try {
await this.appStateListener.remove();
logger.log("App state change listener removed successfully");
} catch (error) {
logger.error("Error removing app state change listener:", error);
logger.error("Error loading initial data:", error);
throw error;
}
}
try {
await App.removeAllListeners();
logger.log("All app listeners removed successfully");
} catch (error) {
logger.error("Error removing all app listeners:", error);
}
};
// Cleanup everything
Promise.all([cleanupListeners(), this.cleanupCamera()]).catch((error) => {
logger.error("Error during component cleanup:", error);
async openQRScanner() {
this.$refs.qrScannerDialog.open(async (rawValue: string) => {
await this.handleScanResult(rawValue);
});
}
async openMobileCamera() {
try {
this.state.isProcessing = true;
this.state.processingStatus = "Starting camera...";
logger.log("Opening mobile camera - starting initialization");
// Check current permission status
const status = await BarcodeScanner.checkPermissions();
logger.log("Camera permission status:", JSON.stringify(status, null, 2));
if (status.camera !== "granted") {
// Request permission if not granted
logger.log("Requesting camera permissions...");
const permissionStatus = await BarcodeScanner.requestPermissions();
if (permissionStatus.camera !== "granted") {
throw new Error("Camera permission not granted");
}
logger.log(
"Camera permission granted:",
JSON.stringify(permissionStatus, null, 2),
);
}
// Remove any existing listener first
try {
if (this.scanListener) {
logger.log("Removing existing barcode listener");
await this.scanListener.remove();
this.scanListener = null;
}
} catch (error) {
logger.error("Error removing existing listener:", error);
// Continue with setup even if removal fails
}
// Set up the listener before starting the scan
logger.log("Setting up new barcode listener");
this.scanListener = await BarcodeScanner.addListener(
"barcodesScanned",
async (result: ScanResult) => {
logger.log(
"Barcode scan result received:",
JSON.stringify(result, null, 2),
);
if (result.barcodes && result.barcodes.length > 0) {
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
await this.handleScanResult(result.barcodes[0].rawValue);
}
},
);
logger.log("Barcode listener setup complete");
// Start the scanner
logger.log("Starting barcode scanner");
await BarcodeScanner.startScan();
logger.log("Barcode scanner started successfully");
this.state.isProcessing = false;
this.state.processingStatus = "";
} catch (error) {
logger.error("Failed to open camera:", error);
this.state.isProcessing = false;
this.state.processingStatus = "";
this.state.scannerState.status = "error";
this.showError(
error instanceof Error ? error.message : "Failed to open camera",
);
// Cleanup on error
try {
if (this.scanListener) {
await this.scanListener.remove();
this.scanListener = null;
}
} catch (cleanupError) {
logger.error("Error during cleanup:", cleanupError);
}
}
}
private async handleScanResult(rawValue: string) {
try {
this.state.isProcessing = true;
this.state.processingStatus = "Processing QR code...";
this.state.processingDetails = `Scanned value: ${rawValue}`;
// Stop scanning before processing
await this.stopScanning();
// Process the scan result
await this.onScanDetect({ rawValue });
} catch (error) {
logger.error("Error handling scan result:", error);
this.showError("Failed to process scan result");
} finally {
this.state.isProcessing = false;
this.state.processingStatus = "";
this.state.processingDetails = "";
}
const result = await this.processQRCode(rawValue);
if (!result) {
return;
}
private logWithDetails(message: string, details?: Record<string, unknown>) {
const formattedDetails = details
? "\n" +
JSON.stringify(
details,
(_key, value) => {
if (value instanceof Error) {
return {
message: value.message,
stack: value.stack,
name: value.name,
};
}
return value;
},
2,
)
: "";
// Wait a short moment to ensure QR dialog is fully closed
await new Promise((resolve) => setTimeout(resolve, 150));
logger.log(`${message}${formattedDetails}`);
if (result.isNewContact) {
await this.openUsernameDialog(result.contact);
}
danger(message: string, title = "Error", timeout = 5000): void {
this.showSuccessNotification(result.contact);
} catch (error) {
logger.error("Error handling QR scan result:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: title,
text: message,
title: "Error",
text: "Failed to process QR code. Please try again.",
},
timeout,
5000,
);
}
/**
*
* @param content is the result of a QR scan, an array with one item with a rawValue property
*/
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async onScanDetect(content: any): Promise<void> {
// Log the received content for debugging
logger.log("Scan result received:", JSON.stringify(content, null, 2));
// Handle both array format and direct object format
let rawValue: string | undefined;
if (Array.isArray(content)) {
rawValue = content[0]?.rawValue;
logger.log("Processing array format, rawValue:", rawValue);
} else if (content?.barcodes?.[0]?.rawValue) {
rawValue = content.barcodes[0].rawValue;
logger.log("Processing barcodes array format, rawValue:", rawValue);
} else if (content?.rawValue) {
rawValue = content.rawValue;
logger.log("Processing object format, rawValue:", rawValue);
} else if (typeof content === "string") {
rawValue = content;
logger.log("Processing string format, rawValue:", rawValue);
}
if (!rawValue) {
logger.error("No valid QR code content found in:", content);
this.danger("No QR code detected. Please try again.", "Scan Error");
return;
}
private async processQRCode(rawValue: string) {
try {
// Validate URL format first
if (!rawValue.startsWith("http://") && !rawValue.startsWith("https://")) {
logger.error("Invalid URL format:", rawValue);
@ -633,253 +264,180 @@ export default class ContactQRScanShowView extends Vue {
"Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
"Invalid Format",
);
return;
return null;
}
let newContact: Contact;
try {
// Extract JWT from URL
const jwt = getContactJwtFromJwtUrl(rawValue);
if (!jwt) {
logger.error("Failed to extract JWT from URL:", rawValue);
this.danger(
"Could not extract contact information from the QR code. Please try again.",
"Invalid QR Code",
);
return;
}
// Log JWT for debugging
logger.log("Extracted JWT:", jwt);
const jwt = await getContactJwtFromJwtUrl(rawValue);
const decoded = await decodeEndorserJwt(jwt);
const did = decoded.payload.iss;
// Validate JWT format
if (
!jwt.match(/^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/)
) {
logger.error("Invalid JWT format:", jwt);
this.danger(
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
"Invalid Data",
);
return;
if (!isDid(did)) {
throw new Error("Invalid DID format");
}
const { payload } = decodeEndorserJwt(jwt);
if (!payload) {
logger.error("Failed to decode JWT payload");
this.danger(
"Could not decode the contact information. Please try again.",
"Decode Error",
);
return;
}
// Log decoded payload for debugging
logger.log("Decoded JWT payload:", JSON.stringify(payload, null, 2));
// Check if contact already exists
const existingContact = await db.contacts
.where("did")
.equals(did)
.first();
// Validate required fields
if (!payload.own && !payload.iss) {
logger.error("Missing required fields in payload:", payload);
this.danger(
"Missing required contact information. Please scan a valid TimeSafari contact QR code.",
"Incomplete Data",
if (existingContact) {
this.$notify(
{
group: "alert",
type: "info",
title: "Contact Exists",
text: `${existingContact.name} is already in your contacts.`,
},
5000,
);
return;
return null;
}
// Create new contact
newContact = {
did: payload.own?.did || payload.iss,
name: payload.own?.name,
nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
profileImageUrl: payload.own?.profileImageUrl,
publicKeyBase64: payload.own?.publicEncKey,
registered: payload.own?.registered,
did: did,
name: "",
publicKeyBase64: "",
registered: false,
};
if (!newContact.did) {
this.danger(
"Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
"Incomplete Contact",
);
return;
}
if (!isDid(newContact.did)) {
this.danger(
"Invalid contact identifier format. The identifier must begin with 'did:'.",
"Invalid Identifier",
);
return;
}
await db.open();
// Save contact
await db.contacts.add(newContact);
let addedMessage;
if (this.activeDid) {
await this.setVisibility(newContact, true);
newContact.seesMe = true;
addedMessage = "They were added, and your activity is visible to them.";
} else {
addedMessage = "They were added.";
// Ask for contact name
this.$refs.userNameDialog.open(async (name: string) => {
if (name) {
await db.contacts.where("did").equals(did).modify({ name });
}
// Check if user needs to register
if (!this.isRegistered && !this.hideRegisterPromptOnNewContact) {
this.$notify(
{
group: "alert",
type: "info",
title: "Register Your Account",
text: "Would you like to register your account now? This will allow your new contact to see your activity.",
onYes: async () => {
try {
const result = await register(
this.activeDid,
this.apiServer,
this.axios,
newContact,
);
if (result.success) {
newContact.registered = true;
await db.contacts.update(newContact.did, {
registered: true,
});
this.isRegistered = true;
this.$notify(
{
group: "alert",
type: "success",
title: "Contact Added",
text: addedMessage,
title: "Registration Success",
text: "The contact has been registered.",
},
3000,
);
if (
this.isRegistered &&
!this.hideRegisterPromptOnNewContact &&
!newContact.registered
) {
setTimeout(() => {
} else {
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;
group: "alert",
type: "warning",
title: "Registration Failed",
text:
result.error || "Failed to register the contact.",
},
5000,
);
}
} catch (error) {
logger.error("Error registering:", error);
this.danger("Failed to register your account");
}
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
onNo: async () => {
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
hideRegisterPromptOnNewContact: true,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onYes: async () => {
await this.register(newContact);
this.hideRegisterPromptOnNewContact = true;
},
promptToStopAsking: true,
},
-1,
);
}, 500);
}
} catch (e) {
logger.error("Error processing QR code:", e);
this.danger(
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.",
"Processing Error",
);
}
}
async setVisibility(contact: Contact, visibility: boolean): Promise<void> {
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.error("Got strange result from setting visibility:", result);
}
}
});
async register(contact: Contact): Promise<void> {
// Show success message
this.$notify(
{
group: "alert",
type: "toast",
text: "",
title: "Registration submitted...",
type: "success",
title: "Contact Added",
text: "Contact has been added to your list.",
},
1000,
3000,
);
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 });
return {
isNewContact: true,
contact: newContact,
};
} catch (error) {
logger.error("Error processing contact:", error);
this.danger("Failed to process contact information");
return null;
}
} catch (error) {
logger.error("Error handling scan result:", error);
this.danger("Failed to process QR code");
return null;
}
}
private async openUsernameDialog(_contact: Contact) {
// Implementation of openUsernameDialog method
}
private showSuccessNotification(contact: Contact) {
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.",
title: "Contact Added",
text: contact.name
? `${contact.name} has been added to your contacts.`
: "New contact has been added to your list.",
},
5000,
3000,
);
}
} catch (error) {
logger.error("Error when registering:", error);
let userMessage = "There was an error.";
const serverError = error as AxiosError;
if (serverError) {
const responseData = serverError.response?.data as {
error?: { message?: string };
};
if (responseData?.error?.message) {
userMessage = responseData.error.message;
} else if (serverError.message) {
userMessage = serverError.message;
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
danger(message: string, title = "Error", timeout = 5000): void {
this.$notify(
{
group: "alert",
type: "danger",
title: "Registration Error",
text: userMessage,
title: title,
text: message,
},
5000,
timeout,
);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any): void {
logger.error("Scan was invalid:", error);
private showError(message: string) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Invalid Scan",
text: "The scan was invalid.",
title: "Error",
text: message,
},
5000,
);
@ -887,7 +445,7 @@ export default class ContactQRScanShowView extends Vue {
async onCopyUrlToClipboard(): Promise<void> {
try {
await this.platformService.writeToClipboard(this.qrValue);
await navigator.clipboard.writeText(this.qrValue);
this.$notify(
{
group: "alert",
@ -905,7 +463,7 @@ export default class ContactQRScanShowView extends Vue {
async onCopyDidToClipboard(): Promise<void> {
try {
await this.platformService.writeToClipboard(this.activeDid);
await navigator.clipboard.writeText(this.activeDid);
this.$notify(
{
group: "alert",
@ -921,31 +479,6 @@ export default class ContactQRScanShowView extends Vue {
}
}
async copyToClipboard(text: string): Promise<void> {
try {
await this.platformService.writeToClipboard(text);
this.$notify(
{
group: "alert",
type: "success",
title: "Copied to clipboard",
text: "The DID has been copied to your clipboard.",
},
3000,
);
} catch (error) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to copy to clipboard.",
},
3000,
);
}
}
get useQRReader(): boolean {
return __USE_QR_READER__;
}
@ -953,50 +486,5 @@ export default class ContactQRScanShowView extends Vue {
get isMobile(): boolean {
return __IS_MOBILE__;
}
private showError(message: string) {
this.state.error = message;
this.state.scannerState.error = message;
this.state.scannerState.status = "error";
// You might want to show this in your UI or use a toast notification
logger.error(message);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
5000,
);
}
async stopScanning() {
try {
// Remove the listener first
if (this.scanListener) {
await this.scanListener.remove();
this.scanListener = null;
}
// Stop the scanner
await BarcodeScanner.stopScan();
this.state.scannerState.processingStatus = "Scan stopped";
this.state.scannerState.isProcessing = false;
this.state.scannerState.processingDetails = "";
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.state.scannerState.error = `Error stopping scan: ${errorMessage}`;
this.state.scannerState.isProcessing = false;
logger.error("Error stopping scanner:", error);
}
}
private async handleError(error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.state.error = errorMessage;
this.state.scannerState.error = errorMessage;
}
}
</script>

Loading…
Cancel
Save