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. 575
      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", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsqr": "^1.4.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"localstorage-slim": "^2.7.0", "localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
@ -19845,6 +19846,12 @@
"node": "*" "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": { "node_modules/katex": {
"version": "0.16.21", "version": "0.16.21",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", "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", "build:web": "vite build --config vite.config.web.mts",
"electron:dev": "npm run build && electron dist-electron", "electron:dev": "npm run build && electron dist-electron",
"electron:start": "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": "npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb", "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", "electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
@ -95,6 +96,7 @@
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsqr": "^1.4.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"localstorage-slim": "^2.7.0", "localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.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;
}

575
src/views/ContactQRScanShowView.vue

@ -109,26 +109,99 @@ import { PlatformService } from "../services/PlatformService";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import { NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { import {
generateEndorserJwtUrlForAccount,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc"; import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { retrieveAccountMetadata } from "../libs/util";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { logger } from "../utils/logger"; 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 global constants
declare const __USE_QR_READER__: boolean; declare const __USE_QR_READER__: boolean;
declare const __IS_MOBILE__: 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({ @Component({
components: { components: {
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null, QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
@ -158,158 +231,339 @@ export default class ContactQRScanShow extends Vue {
private platformService: PlatformService = private platformService: PlatformService =
PlatformServiceFactory.getInstance(); 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() { async created() {
const settings = await retrieveSettingsForActiveAccount(); logger.log('ContactQRScanShow component created');
this.activeDid = settings.activeDid || ""; try {
this.apiServer = settings.apiServer || ""; // Remove any existing listeners first
this.givenName = settings.firstName || ""; await App.removeAllListeners();
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact; // Add app state listeners
this.isRegistered = !!settings.isRegistered; const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
logger.log('App state changed:', state);
const account = await retrieveAccountMetadata(this.activeDid); if (!state.isActive && this.cameraActive) {
if (account) { this.cleanupCamera();
const name =
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : "");
this.qrValue = await generateEndorserJwtUrlForAccount(
account,
!!settings.isRegistered,
name,
settings.profileImageUrl || "",
false,
);
} }
});
this.appStateListener = stateListener;
// Initialize camera with retry logic await App.addListener('pause', () => {
if (this.useQRReader) { logger.log('App paused');
await this.initializeCamera(); if (this.cameraActive) {
this.cleanupCamera();
} }
});
await App.addListener('resume', () => {
logger.log('App resumed');
if (this.cameraActive) {
this.initializeCamera();
} }
});
async initializeCamera(retryCount = 0): Promise<void> { // Load initial data
try { await this.loadInitialData();
const capabilities = this.platformService.getCapabilities();
if (!capabilities.hasCamera) { logger.log('ContactQRScanShow initialization complete');
this.danger("No camera available on this device.", "Camera Error"); } catch (error) {
return; logger.error('Failed to initialize ContactQRScanShow:', error);
this.showError('Failed to initialize. Please try again.');
}
} }
// Check camera permissions private async loadInitialData() {
const hasPermission = await this.checkCameraPermission(); try {
if (!hasPermission) { // Load settings from DB
this.danger( await db.open();
"Camera permission is required to scan QR codes. Please enable camera access in your device settings.", const settings = await db.settings.get(MASTER_SETTINGS_KEY);
"Permission Required" if (settings) {
); this.hideRegisterPromptOnNewContact = settings.hideRegisterPromptOnNewContact || false;
return; }
logger.log('Initial data loaded successfully');
} catch (error) {
logger.error('Failed to load initial data:', error);
throw error;
}
} }
// If we get here, camera should be available private cleanupCamera() {
this.$notify( try {
{ this.cameraActive = false;
group: "alert", this.isCapturingPhoto = false;
type: "success", this.addCameraState('cleanup');
title: "Camera Ready", logger.log('Camera cleaned up successfully');
text: "Camera is ready to scan QR codes.",
},
3000
);
} catch (error) { } catch (error) {
logger.error("Error initializing camera:", error); logger.error('Error during camera cleanup:', error);
}
}
// Retry up to 3 times for certain errors private addCameraState(state: CameraState | QRProcessingState, details?: Record<string, unknown>) {
if (retryCount < 3) { const entry: CameraStateHistoryEntry = {
const isPermissionError = error instanceof Error && state,
(error.message.includes("permission") || timestamp: Date.now(),
error.message.includes("NotReadableError")); details
};
if (isPermissionError) { this.cameraStateHistory.push(entry);
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000)); if (this.cameraStateHistory.length > this.STATE_HISTORY_LIMIT) {
return this.initializeCamera(retryCount + 1); this.cameraStateHistory.shift();
}
} }
this.danger( // Enhanced logging with better details
"Failed to initialize camera. Please check your camera permissions and try again.", logger.log('Camera state transition:', {
"Camera Error" state,
); details: {
...details,
cameraActive: this.cameraActive,
isCapturingPhoto: this.isCapturingPhoto,
historyLength: this.cameraStateHistory.length
} }
});
this.lastCameraState = state;
} }
async checkCameraPermission(): Promise<boolean> { 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 { try {
const capabilities = this.platformService.getCapabilities(); await this.appStateListener.remove();
if (!capabilities.hasCamera) { logger.log('App state change listener removed successfully');
return false; } catch (error) {
logger.error('Error removing app state change listener:', error);
}
} }
// Try to access camera to check permissions try {
await this.platformService.takePicture(); await App.removeAllListeners();
return true; logger.log('All app listeners removed successfully');
} catch (error) { } catch (error) {
logger.error("Camera permission check failed:", error); logger.error('Error removing all app listeners:', error);
return false;
} }
};
// Cleanup everything
Promise.all([
cleanupListeners(),
this.cleanupCamera()
]).catch(error => {
logger.error('Error during component cleanup:', error);
});
} }
async openMobileCamera(): Promise<void> { private async handleQRCodeResult(data: string): Promise<void> {
try { try {
// Check permissions first await this.onScanDetect([{ rawValue: data }]);
const hasPermission = await this.checkCameraPermission(); } catch (error) {
if (!hasPermission) { console.error('Failed to handle QR code result:', error);
this.danger( this.showError('Failed to process QR code data.');
"Camera permission is required. Please enable camera access in your device settings.", }
"Permission Required" }
);
async initializeCamera(retryCount = 0): Promise<void> {
if (this.cameraActive) {
console.log('Camera already active, skipping initialization');
return; return;
} }
const image = await Camera.getPhoto({ try {
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');
}
this.cameraActive = true;
// Configure camera options
const cameraOptions: ImageOptions = {
quality: 90, quality: 90,
allowEditing: false, allowEditing: false,
resultType: CameraResultType.DataUrl, resultType: CameraResultType.DataUrl,
source: CameraSource.Camera, source: CameraSource.Camera
}); };
if (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); await this.processImageForQRCode(image.dataUrl);
} catch (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 checkCameraPermission(): Promise<{ camera: CameraPermissionState }> {
try {
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) { } catch (error) {
logger.error("Error taking picture:", error); this.addCameraState('permission_check_error');
this.danger( return { camera: 'denied' as CameraPermissionState };
"Failed to access camera. Please check your camera permissions.",
"Camera Error"
);
} }
} }
async processImageForQRCode(_imageDataUrl: string) { 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 { try {
// Here you would implement QR code scanning from the image // Import jsQR in the worker
// For example, using jsQR: importScripts('${window.location.origin}/assets/jsqr.js');
// const image = new Image(); const code = self.jsQR(imageData, width, height, {
// image.src = imageDataUrl; inversionAttempts: "dontInvert"
// image.onload = () => { });
// const canvas = document.createElement('canvas'); self.postMessage({ success: true, code });
// 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 }]);
// }
// };
} catch (error) { } catch (error) {
logger.error("Error processing image for QR code:", error); self.postMessage({ success: false, error: error.message });
this.danger( }
"Failed to process the image. Please try again.", };
"Processing Error", `], { 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');
} }
} }
@ -712,5 +966,96 @@ export default class ContactQRScanShow extends Vue {
get isMobile(): boolean { get isMobile(): boolean {
return __IS_MOBILE__; 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> </script>

4
tsconfig.node.json

@ -4,7 +4,9 @@
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"noEmit": true
}, },
"include": ["vite.config.*"] "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"; 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', assetsDir: 'assets',
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
rollupOptions: { rollupOptions: {
external: isCapacitor ? ['@capacitor/app'] : [] external: []
} }
}, },
define: { define: {
@ -44,10 +44,13 @@ export async function createBuildConfig(mode: string) {
'process.env.VITE_PLATFORM': JSON.stringify(mode), 'process.env.VITE_PLATFORM': JSON.stringify(mode),
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)), 'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""', __dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
__USE_QR_READER__: JSON.stringify(!isCapacitor),
__IS_MOBILE__: JSON.stringify(isCapacitor),
}, },
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
'@capacitor/app': path.resolve(__dirname, 'node_modules/@capacitor/app'),
...appConfig.aliasConfig, ...appConfig.aliasConfig,
'nostr-tools/nip06': mode === 'development' 'nostr-tools/nip06': mode === 'development'
? 'nostr-tools/nip06' ? 'nostr-tools/nip06'

Loading…
Cancel
Save