Browse Source

refactor(qr-scanner): improve QR code scanning with Web Workers and enhanced error handling

- Move QR code processing to Web Worker for better performance
- Add comprehensive camera state tracking and logging
- Implement proper app lifecycle management
- Add image size limits and optimization (max 1024px)
- Add proper cleanup of resources (worker, timeouts, listeners)
- Improve error handling with detailed error messages
- Add TypeScript interfaces for worker messages and QR code results
- Update Vite config to properly handle Capacitor builds
qrcode-capacitor
Matthew Raymer 3 months ago
parent
commit
f5846cbe78
  1. 7
      .cursor/rules/reports.mdc
  2. 16
      .cursor/rules/ts-cross-platform-rule.mdc
  3. BIN
      android/.gradle/file-system.probe
  4. 7
      package-lock.json
  5. 4
      package.json
  6. 34
      src/types/jsqr.d.ts
  7. 747
      src/views/ContactQRScanShowView.vue
  8. 4
      tsconfig.node.json
  9. 13
      vite.config.capacitor.mts
  10. 5
      vite.config.common.mts

7
.cursor/rules/reports.mdc

@ -0,0 +1,7 @@
---
description:
globs:
alwaysApply: false
---
- make reports chronologically in paragraph form without using pronouns or references to people
- use this git command to make a report:

16
.cursor/rules/ts-cross-platform-rule.mdc

@ -0,0 +1,16 @@
---
description:
globs:
alwaysApply: true
---
- all cross platform builds need to conform to [PlatformService.ts](mdc:src/services/PlatformService.ts), and [PlatformServiceFactory.ts](mdc:src/services/PlatformServiceFactory.ts)
- [CapacitorPlatformService.ts](mdc:src/services/platforms/CapacitorPlatformService.ts) is used for mobile both iOS and Android
- [ElectronPlatformService.ts](mdc:src/services/platforms/ElectronPlatformService.ts) is used for cross-platform (Windows, MacOS, and Linux) desktop builds using Electron.
- [WebPlatformService.ts](mdc:src/services/platforms/WebPlatformService.ts) is used for traditional web browsers and PWA (Progressive Web Applications)
- [PyWebViewPlatformService.ts](mdc:src/services/platforms/PyWebViewPlatformService.ts) is used for handling a electron-like desktop application which can run Python
- Vite is used to differentiate builds for platforms
- @vite.config.mts is used for general configuration which uses environment variables to determine next actions
- @vite.config.common.mts handles common features in vite builds.
- [vite.config.capacitor.mts](mdc:vite.config.capacitor.mts) handles features of Vite builds for capacitor
- [vite.config.electron.mts](mdc:vite.config.electron.mts) handles features of Vite builds for electron
- [vite.config.web.mts](mdc:vite.config.web.mts) handles features of Vite builds for traditional web browsers and PWAs

BIN
android/.gradle/file-system.probe

Binary file not shown.

7
package-lock.json

@ -57,6 +57,7 @@
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0",
"jsqr": "^1.4.0",
"leaflet": "^1.9.4",
"localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0",
@ -19845,6 +19846,12 @@
"node": "*"
}
},
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
"license": "Apache-2.0"
},
"node_modules/katex": {
"version": "0.16.21",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",

4
package.json

@ -30,7 +30,8 @@
"build:web": "vite build --config vite.config.web.mts",
"electron:dev": "npm run build && electron dist-electron",
"electron:start": "electron dist-electron",
"build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
"clean:android": "adb uninstall app.timesafari.app || true",
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
@ -95,6 +96,7 @@
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0",
"jsqr": "^1.4.0",
"leaflet": "^1.9.4",
"localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0",

34
src/types/jsqr.d.ts

@ -0,0 +1,34 @@
declare module 'jsqr' {
interface Point {
x: number;
y: number;
}
interface QRLocation {
topLeft: Point;
topRight: Point;
bottomLeft: Point;
bottomRight: Point;
topLeftFinder: Point;
topRightFinder: Point;
bottomLeftFinder: Point;
bottomRightAlignment?: Point;
}
interface QRCode {
binaryData: number[];
data: string;
location: QRLocation;
}
function jsQR(
imageData: Uint8ClampedArray,
width: number,
height: number,
options?: {
inversionAttempts?: 'dontInvert' | 'onlyInvert' | 'attemptBoth';
}
): QRCode | null;
export default jsQR;
}

747
src/views/ContactQRScanShowView.vue

@ -109,26 +109,99 @@ import { PlatformService } from "../services/PlatformService";
import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
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 {
generateEndorserJwtUrlForAccount,
isDid,
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 { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { Camera, CameraResultType, CameraSource, ImageOptions, CameraPermissionState } from '@capacitor/camera';
import { App } from '@capacitor/app';
import jsQR from "jsqr";
// 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>;
}
// Custom AppState type to match our needs
interface CustomAppState {
state: 'active' | 'inactive' | 'background' | 'foreground';
}
interface AppStateChangeEvent {
isActive: boolean;
}
// Define worker message types
interface QRCodeResult {
data: string;
location: {
topLeftCorner: { x: number; y: number };
topRightCorner: { x: number; y: number };
bottomRightCorner: { x: number; y: number };
bottomLeftCorner: { x: number; y: number };
};
}
interface WorkerSuccessMessage {
success: true;
code: QRCodeResult;
}
interface WorkerErrorMessage {
success: false;
error: string;
}
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage;
@Component({
components: {
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
@ -158,158 +231,339 @@ export default class ContactQRScanShow extends Vue {
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> };
async created() {
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,
);
}
logger.log('ContactQRScanShow component created');
try {
// Remove any existing listeners first
await App.removeAllListeners();
// Initialize camera with retry logic
if (this.useQRReader) {
await this.initializeCamera();
// Add app state listeners
const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
logger.log('App state changed:', state);
if (!state.isActive && this.cameraActive) {
this.cleanupCamera();
}
});
this.appStateListener = stateListener;
await App.addListener('pause', () => {
logger.log('App paused');
if (this.cameraActive) {
this.cleanupCamera();
}
});
await App.addListener('resume', () => {
logger.log('App resumed');
if (this.cameraActive) {
this.initializeCamera();
}
});
// Load initial data
await this.loadInitialData();
logger.log('ContactQRScanShow initialization complete');
} catch (error) {
logger.error('Failed to initialize ContactQRScanShow:', error);
this.showError('Failed to initialize. Please try again.');
}
}
async initializeCamera(retryCount = 0): Promise<void> {
private async loadInitialData() {
try {
const capabilities = this.platformService.getCapabilities();
if (!capabilities.hasCamera) {
this.danger("No camera available on this device.", "Camera Error");
return;
// 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;
}
}
// Check camera permissions
const hasPermission = await this.checkCameraPermission();
if (!hasPermission) {
this.danger(
"Camera permission is required to scan QR codes. Please enable camera access in your device settings.",
"Permission Required"
);
return;
private cleanupCamera() {
try {
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>) {
const entry: CameraStateHistoryEntry = {
state,
timestamp: Date.now(),
details
};
this.cameraStateHistory.push(entry);
if (this.cameraStateHistory.length > this.STATE_HISTORY_LIMIT) {
this.cameraStateHistory.shift();
}
// Enhanced logging with better details
logger.log('Camera state transition:', {
state,
details: {
...details,
cameraActive: this.cameraActive,
isCapturingPhoto: this.isCapturingPhoto,
historyLength: this.cameraStateHistory.length
}
});
// If we get here, camera should be available
this.$notify(
{
group: "alert",
type: "success",
title: "Camera Ready",
text: "Camera is ready to scan QR codes.",
},
3000
);
} catch (error) {
logger.error("Error initializing camera:", error);
// Retry up to 3 times for certain errors
if (retryCount < 3) {
const isPermissionError = error instanceof Error &&
(error.message.includes("permission") ||
error.message.includes("NotReadableError"));
if (isPermissionError) {
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
return this.initializeCamera(retryCount + 1);
this.lastCameraState = state;
}
beforeDestroy() {
logger.log('ContactQRScanShow component being destroyed, initiating cleanup', {
cameraActive: this.cameraActive,
lastState: this.lastCameraState,
stateHistory: this.cameraStateHistory
});
// 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);
}
}
this.danger(
"Failed to initialize camera. Please check your camera permissions and try again.",
"Camera 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 checkCameraPermission(): Promise<boolean> {
private async handleQRCodeResult(data: string): Promise<void> {
try {
const capabilities = this.platformService.getCapabilities();
if (!capabilities.hasCamera) {
return false;
}
// Try to access camera to check permissions
await this.platformService.takePicture();
return true;
await this.onScanDetect([{ rawValue: data }]);
} catch (error) {
logger.error("Camera permission check failed:", error);
return false;
console.error('Failed to handle QR code result:', error);
this.showError('Failed to process QR code data.');
}
}
async openMobileCamera(): Promise<void> {
async initializeCamera(retryCount = 0): Promise<void> {
if (this.cameraActive) {
console.log('Camera already active, skipping initialization');
return;
}
try {
// Check permissions first
const hasPermission = await this.checkCameraPermission();
if (!hasPermission) {
this.danger(
"Camera permission is required. Please enable camera access in your device settings.",
"Permission Required"
);
return;
console.log('Initializing camera...', { retryCount });
// Check camera permissions first
const permissionStatus = await this.checkCameraPermission();
if (permissionStatus.camera !== 'granted') {
throw new Error('Camera permission not granted');
}
const image = await Camera.getPhoto({
this.cameraActive = true;
// Configure camera options
const cameraOptions: ImageOptions = {
quality: 90,
allowEditing: false,
resultType: CameraResultType.DataUrl,
source: CameraSource.Camera,
});
source: CameraSource.Camera
};
if (image.dataUrl) {
await this.processImageForQRCode(image.dataUrl);
console.log('Opening camera with options:', cameraOptions);
const image = await Camera.getPhoto(cameraOptions);
if (!image || !image.dataUrl) {
throw new Error('Failed to capture photo: No image data received');
}
console.log('Photo captured successfully, processing image...');
await this.processImageForQRCode(image.dataUrl);
} catch (error) {
logger.error("Error taking picture:", error);
this.danger(
"Failed to access camera. Please check your camera permissions.",
"Camera Error"
);
this.cameraActive = false;
console.error('Camera initialization failed:', error instanceof Error ? error.message : String(error));
// Handle user cancellation separately
if (error instanceof Error && error.message.includes('User cancelled photos app')) {
console.log('User cancelled photo capture');
return;
}
// Handle retry logic
if (retryCount < 2) {
console.log('Retrying camera initialization...');
await new Promise(resolve => setTimeout(resolve, 1000));
return this.initializeCamera(retryCount + 1);
}
this.showError('Failed to initialize camera. Please try again.');
}
}
async processImageForQRCode(_imageDataUrl: string) {
async checkCameraPermission(): Promise<{ camera: CameraPermissionState }> {
try {
// Here you would implement QR code scanning from the image
// For example, using jsQR:
// const image = new Image();
// image.src = imageDataUrl;
// image.onload = () => {
// const canvas = document.createElement('canvas');
// const context = canvas.getContext('2d');
// canvas.width = image.width;
// canvas.height = image.height;
// context.drawImage(image, 0, 0);
// const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// const code = jsQR(imageData.data, imageData.width, imageData.height);
// if (code) {
// this.onScanDetect([{ rawValue: code.data }]);
// }
// };
this.addCameraState('checking_permissions');
const capabilities = this.platformService.getCapabilities();
if (!capabilities.hasCamera) {
this.addCameraState('no_camera_capability');
return { camera: 'denied' as CameraPermissionState };
}
const permissionStatus = await Camera.checkPermissions();
this.addCameraState('permission_status_checked');
if (permissionStatus.camera === 'prompt') {
this.addCameraState('requesting_permission');
const requestResult = await Camera.requestPermissions();
this.addCameraState('permission_requested');
return requestResult;
}
return permissionStatus;
} catch (error) {
logger.error("Error processing image for QR code:", error);
this.danger(
"Failed to process the image. Please try again.",
"Processing Error",
);
this.addCameraState('permission_check_error');
return { camera: 'denied' as CameraPermissionState };
}
}
async processImageForQRCode(imageDataUrl: string): Promise<void> {
try {
logger.log('Starting QR code processing');
// Create worker for image processing
const worker = new Worker(URL.createObjectURL(new Blob([`
self.onmessage = async function(e) {
const { imageData, width, height } = e.data;
try {
// Import jsQR in the worker
importScripts('${window.location.origin}/assets/jsqr.js');
const code = self.jsQR(imageData, width, height, {
inversionAttempts: "dontInvert"
});
self.postMessage({ success: true, code });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
`], { type: 'text/javascript' })));
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = imageDataUrl;
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Image load timeout')), 5000);
image.onload = () => {
clearTimeout(timeout);
resolve(undefined);
};
image.onerror = () => {
clearTimeout(timeout);
reject(new Error('Failed to load image'));
};
});
logger.log('Image loaded, creating canvas...');
const canvas = document.createElement('canvas');
const maxDimension = 1024; // Limit image size for better performance
// Scale down image if needed while maintaining aspect ratio
let width = image.naturalWidth || 800;
let height = image.naturalHeight || 600;
if (width > maxDimension || height > maxDimension) {
if (width > height) {
height = Math.floor(height * (maxDimension / width));
width = maxDimension;
} else {
width = Math.floor(width * (maxDimension / height));
height = maxDimension;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
// Draw image maintaining orientation
ctx.save();
ctx.drawImage(image, 0, 0, width, height);
ctx.restore();
const imageData = ctx.getImageData(0, 0, width, height);
logger.log('Processing image data for QR code...', {
width,
height,
dataLength: imageData.data.length
});
// Process QR code in worker
const result = await new Promise<QRCodeResult>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
if (e.data.success) {
resolve(e.data.code);
} else {
reject(new Error(e.data.error));
}
};
worker.onerror = reject;
worker.postMessage({
imageData: imageData.data,
width: imageData.width,
height: imageData.height
});
});
worker.terminate();
if (result) {
logger.log('QR code found:', { data: result.data });
await this.handleQRCodeResult(result.data);
} else {
logger.log('No QR code found in image');
this.showError('No QR code found. Please try again.');
}
} catch (error) {
logger.error('QR code processing failed:', error);
this.showError('Failed to process QR code. Please try again.');
} finally {
this.cameraActive = false;
this.isCapturingPhoto = false;
this.addCameraState('processing_completed');
}
}
@ -347,11 +601,11 @@ export default class ContactQRScanShow extends Vue {
return;
}
let newContact: Contact;
try {
let newContact: Contact;
try {
// Extract JWT from URL
const jwt = getContactJwtFromJwtUrl(url);
if (!jwt) {
const jwt = getContactJwtFromJwtUrl(url);
if (!jwt) {
this.danger(
"Could not extract contact information from the QR code. Please try again.",
"Invalid QR Code",
@ -366,11 +620,11 @@ export default class ContactQRScanShow extends Vue {
this.danger(
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
"Invalid Data",
);
return;
}
);
return;
}
const { payload } = decodeEndorserJwt(jwt);
const { payload } = decodeEndorserJwt(jwt);
if (!payload) {
this.danger(
"Could not decode the contact information. Please try again.",
@ -388,7 +642,7 @@ export default class ContactQRScanShow extends Vue {
return;
}
newContact = {
newContact = {
did: payload.own?.did || payload.iss,
name: payload.own?.name,
nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
@ -397,15 +651,15 @@ export default class ContactQRScanShow extends Vue {
registered: payload.own?.registered,
};
if (!newContact.did) {
if (!newContact.did) {
this.danger(
"Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
"Incomplete Contact",
);
return;
}
return;
}
if (!isDid(newContact.did)) {
if (!isDid(newContact.did)) {
this.danger(
"Invalid contact identifier format. The identifier must begin with 'did:'.",
"Invalid Identifier",
@ -413,66 +667,66 @@ export default class ContactQRScanShow extends Vue {
return;
}
await db.open();
await db.contacts.add(newContact);
await db.open();
await db.contacts.add(newContact);
let addedMessage;
if (this.activeDid) {
await this.setVisibility(newContact, true);
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.";
}
} else {
addedMessage = "They were added.";
}
this.$notify(
{
group: "alert",
type: "success",
title: "Contact Added",
text: addedMessage,
},
3000,
);
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?",
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;
}
},
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) {
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.",
@ -589,15 +843,15 @@ export default class ContactQRScanShow extends Vue {
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,
);
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");
@ -607,15 +861,15 @@ export default class ContactQRScanShow extends Vue {
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,
);
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");
@ -712,5 +966,96 @@ export default class ContactQRScanShow extends Vue {
get isMobile(): boolean {
return __IS_MOBILE__;
}
private showError(message: string) {
// Implement the logic to show a user-friendly error message to the user
console.error(message);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
5000,
);
}
async openMobileCamera() {
if (this.isCapturingPhoto) {
this.addCameraState('capture_already_in_progress');
logger.warn('Camera capture already in progress, ignoring request');
return;
}
try {
this.isCapturingPhoto = true;
this.addCameraState('opening_camera');
const config = {
quality: 90,
allowEditing: false,
resultType: CameraResultType.DataUrl,
source: CameraSource.Camera
};
logger.log('Camera configuration:', config);
const image = await Camera.getPhoto(config);
this.addCameraState('photo_captured', {
hasDataUrl: !!image.dataUrl,
dataUrlLength: image.dataUrl?.length,
format: image.format,
saved: image.saved,
webPath: image.webPath,
base64String: image.dataUrl?.substring(0, 50) + '...' // Log first 50 chars of base64
});
if (image.dataUrl) {
this.addCameraState('processing_photo');
await this.processImageForQRCode(image.dataUrl);
} else {
this.addCameraState('no_image_data');
logger.error('Camera returned no image data');
this.$notify({
type: 'error',
title: 'Camera Error',
text: 'No image was captured. Please try again.',
group: 'qr-scanner'
});
}
} catch (error) {
this.addCameraState('capture_error', {
error,
errorName: error instanceof Error ? error.name : 'Unknown',
errorMessage: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined
});
if (error instanceof Error) {
if (error.message.includes('User cancelled photos app')) {
logger.log('User cancelled photo capture');
this.addCameraState('user_cancelled');
} else if (error.message.includes('permission')) {
logger.error('Camera permission error during capture');
this.addCameraState('permission_error');
} else if (error.message.includes('Camera is not available')) {
logger.error('Camera hardware not available');
this.addCameraState('hardware_unavailable');
}
}
this.$notify({
type: 'error',
title: 'Camera Error',
text: 'Failed to capture photo. Please check camera permissions and try again.',
group: 'qr-scanner'
});
} finally {
this.isCapturingPhoto = false;
this.addCameraState('capture_completed');
}
}
}
</script>

4
tsconfig.node.json

@ -4,7 +4,9 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["vite.config.*"]
}

13
vite.config.capacitor.mts

@ -1,4 +1,13 @@
import { defineConfig } from "vite";
import { defineConfig, mergeConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
export default defineConfig(async () => createBuildConfig('capacitor'));
export default defineConfig(async () => {
const baseConfig = await createBuildConfig('capacitor');
return mergeConfig(baseConfig, {
define: {
__USE_QR_READER__: false, // Disable web QR reader on mobile
__IS_MOBILE__: true, // Enable mobile-specific features
}
});
});

5
vite.config.common.mts

@ -36,7 +36,7 @@ export async function createBuildConfig(mode: string) {
assetsDir: 'assets',
chunkSizeWarningLimit: 1000,
rollupOptions: {
external: isCapacitor ? ['@capacitor/app'] : []
external: []
}
},
define: {
@ -44,10 +44,13 @@ export async function createBuildConfig(mode: string) {
'process.env.VITE_PLATFORM': JSON.stringify(mode),
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
__USE_QR_READER__: JSON.stringify(!isCapacitor),
__IS_MOBILE__: JSON.stringify(isCapacitor),
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@capacitor/app': path.resolve(__dirname, 'node_modules/@capacitor/app'),
...appConfig.aliasConfig,
'nostr-tools/nip06': mode === 'development'
? 'nostr-tools/nip06'

Loading…
Cancel
Save