forked from trent_larson/crowd-funder-for-time-pwa
- Reorganize QR scanner to follow platform service pattern - Move QR scanner implementations to services/platforms directory - Update QRScannerFactory to use PlatformServiceFactory pattern - Consolidate common QR scanner interfaces with platform services - Add proper error handling and logging across implementations - Ensure consistent cleanup and lifecycle management - Add PyWebView implementation placeholder for desktop - Update Capacitor and Web implementations to match platform patterns
1003 lines
29 KiB
Vue
1003 lines
29 KiB
Vue
<template>
|
|
<QuickNav selected="Profile" />
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Processing Overlay -->
|
|
<div
|
|
v-if="state.isProcessing"
|
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
>
|
|
<div class="bg-white rounded-lg p-6 text-center">
|
|
<div
|
|
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"
|
|
></div>
|
|
<p class="text-lg font-semibold">{{ state.processingStatus }}</p>
|
|
<p class="text-sm text-gray-600 mt-2">{{ state.processingDetails }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Breadcrumb -->
|
|
<div class="mb-8">
|
|
<!-- Back -->
|
|
<div class="relative px-7">
|
|
<h1
|
|
class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
|
|
@click="$router.back()"
|
|
>
|
|
<font-awesome icon="chevron-left" class="fa-fw" />
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Heading -->
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
|
Your Contact Info
|
|
</h1>
|
|
<p
|
|
v-if="!givenName"
|
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-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="() => $refs.userNameDialog.open((name) => (givenName = name))"
|
|
>
|
|
click here to set it for them.
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<UserNameDialog ref="userNameDialog" />
|
|
|
|
<div
|
|
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
|
|
class="text-center"
|
|
@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"
|
|
:corners-square-options="{ type: 'extra-rounded' }"
|
|
:dots-options="{ type: 'square' }"
|
|
class="flex justify-center"
|
|
/>
|
|
<span>
|
|
Click the QR code to copy your contact info to your clipboard.
|
|
</span>
|
|
</div>
|
|
<div v-else-if="activeDid" class="text-center">
|
|
<!-- 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">
|
|
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">
|
|
<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"
|
|
>
|
|
Open Camera
|
|
</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>
|
|
</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";
|
|
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 { 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";
|
|
|
|
// 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;
|
|
denied: boolean;
|
|
isProcessing: boolean;
|
|
processingStatus: string;
|
|
processingDetails: string;
|
|
error: string;
|
|
status: string;
|
|
}
|
|
|
|
interface AppState {
|
|
isProcessing: boolean;
|
|
processingStatus: string;
|
|
processingDetails: string;
|
|
error: string;
|
|
scannerState: ScannerState;
|
|
}
|
|
|
|
@Component({
|
|
components: {
|
|
QRCodeVue3,
|
|
QrcodeStream,
|
|
QuickNav,
|
|
UserNameDialog,
|
|
},
|
|
})
|
|
export default class ContactQRScanShowView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$router!: Router;
|
|
declare $refs: {
|
|
userNameDialog: {
|
|
open: (callback: (name: string) => void) => void;
|
|
};
|
|
};
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
givenName = "";
|
|
hideRegisterPromptOnNewContact = false;
|
|
isRegistered = false;
|
|
qrValue = "";
|
|
|
|
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: "",
|
|
processingDetails: "",
|
|
error: "",
|
|
scannerState: {
|
|
isSupported: false,
|
|
granted: false,
|
|
denied: false,
|
|
isProcessing: false,
|
|
processingStatus: "",
|
|
processingDetails: "",
|
|
error: "",
|
|
status: "",
|
|
},
|
|
});
|
|
|
|
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> {
|
|
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;
|
|
}
|
|
}
|
|
|
|
private async loadInitialData() {
|
|
try {
|
|
// Load settings from DB
|
|
await db.open();
|
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
if (settings) {
|
|
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);
|
|
|
|
if (this.cameraStateHistory.length > this.STATE_HISTORY_LIMIT) {
|
|
this.cameraStateHistory.shift();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 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 = "";
|
|
}
|
|
}
|
|
|
|
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,
|
|
)
|
|
: "";
|
|
|
|
logger.log(`${message}${formattedDetails}`);
|
|
}
|
|
|
|
danger(message: string, title = "Error", timeout = 5000): void {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: title,
|
|
text: message,
|
|
},
|
|
timeout,
|
|
);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
// Validate URL format first
|
|
if (!rawValue.startsWith("http://") && !rawValue.startsWith("https://")) {
|
|
logger.error("Invalid URL format:", rawValue);
|
|
this.danger(
|
|
"Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
|
|
"Invalid Format",
|
|
);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
|
|
// 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;
|
|
}
|
|
|
|
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));
|
|
|
|
// 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",
|
|
);
|
|
return;
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
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();
|
|
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.";
|
|
}
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Contact Added",
|
|
text: addedMessage,
|
|
},
|
|
3000,
|
|
);
|
|
|
|
if (
|
|
this.isRegistered &&
|
|
!this.hideRegisterPromptOnNewContact &&
|
|
!newContact.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(newContact);
|
|
},
|
|
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> {
|
|
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 });
|
|
|
|
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 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;
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Registration Error",
|
|
text: userMessage,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
onScanError(error: any): void {
|
|
logger.error("Scan was invalid:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Invalid Scan",
|
|
text: "The scan was invalid.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
async onCopyUrlToClipboard(): Promise<void> {
|
|
try {
|
|
await this.platformService.writeToClipboard(this.qrValue);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
title: "Copied",
|
|
text: "Contact URL was copied to clipboard.",
|
|
},
|
|
2000,
|
|
);
|
|
} catch (error) {
|
|
logger.error("Error copying to clipboard:", error);
|
|
this.danger("Failed to copy to clipboard", "Error");
|
|
}
|
|
}
|
|
|
|
async onCopyDidToClipboard(): Promise<void> {
|
|
try {
|
|
await this.platformService.writeToClipboard(this.activeDid);
|
|
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,
|
|
);
|
|
} catch (error) {
|
|
logger.error("Error copying to clipboard:", error);
|
|
this.danger("Failed to copy to clipboard", "Error");
|
|
}
|
|
}
|
|
|
|
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__;
|
|
}
|
|
|
|
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>
|