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
This commit is contained in:
7
.cursor/rules/reports.mdc
Normal file
7
.cursor/rules/reports.mdc
Normal file
@@ -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
Normal file
16
.cursor/rules/ts-cross-platform-rule.mdc
Normal file
@@ -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
|
||||||
Binary file not shown.
7
package-lock.json
generated
7
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
vendored
Normal file
34
src/types/jsqr.d.ts
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
this.isRegistered = !!settings.isRegistered;
|
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
// Add app state listeners
|
||||||
if (account) {
|
const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
|
||||||
const name =
|
logger.log('App state changed:', state);
|
||||||
(settings.firstName || "") +
|
if (!state.isActive && this.cameraActive) {
|
||||||
(settings.lastName ? ` ${settings.lastName}` : "");
|
this.cleanupCamera();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.appStateListener = stateListener;
|
||||||
|
|
||||||
this.qrValue = await generateEndorserJwtUrlForAccount(
|
await App.addListener('pause', () => {
|
||||||
account,
|
logger.log('App paused');
|
||||||
!!settings.isRegistered,
|
if (this.cameraActive) {
|
||||||
name,
|
this.cleanupCamera();
|
||||||
settings.profileImageUrl || "",
|
}
|
||||||
false,
|
});
|
||||||
);
|
|
||||||
|
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.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize camera with retry logic
|
private async loadInitialData() {
|
||||||
if (this.useQRReader) {
|
try {
|
||||||
await this.initializeCamera();
|
// 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 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleQRCodeResult(data: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.onScanDetect([{ rawValue: data }]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to handle QR code result:', error);
|
||||||
|
this.showError('Failed to process QR code data.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeCamera(retryCount = 0): Promise<void> {
|
async initializeCamera(retryCount = 0): Promise<void> {
|
||||||
|
if (this.cameraActive) {
|
||||||
|
console.log('Camera already active, skipping initialization');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const capabilities = this.platformService.getCapabilities();
|
console.log('Initializing camera...', { retryCount });
|
||||||
if (!capabilities.hasCamera) {
|
|
||||||
this.danger("No camera available on this device.", "Camera Error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Check camera permissions first
|
||||||
if (retryCount < 3) {
|
const permissionStatus = await this.checkCameraPermission();
|
||||||
const isPermissionError = error instanceof Error &&
|
if (permissionStatus.camera !== 'granted') {
|
||||||
(error.message.includes("permission") ||
|
throw new Error('Camera permission not granted');
|
||||||
error.message.includes("NotReadableError"));
|
|
||||||
|
|
||||||
if (isPermissionError) {
|
|
||||||
// Wait before retrying
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
return this.initializeCamera(retryCount + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.danger(
|
this.cameraActive = true;
|
||||||
"Failed to initialize camera. Please check your camera permissions and try again.",
|
|
||||||
"Camera Error"
|
// Configure camera options
|
||||||
);
|
const cameraOptions: ImageOptions = {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkCameraPermission(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const capabilities = this.platformService.getCapabilities();
|
|
||||||
if (!capabilities.hasCamera) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to access camera to check permissions
|
|
||||||
await this.platformService.takePicture();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Camera permission check failed:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async openMobileCamera(): Promise<void> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = await Camera.getPhoto({
|
|
||||||
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);
|
||||||
await this.processImageForQRCode(image.dataUrl);
|
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) {
|
} catch (error) {
|
||||||
logger.error("Error taking picture:", error);
|
this.cameraActive = false;
|
||||||
this.danger(
|
console.error('Camera initialization failed:', error instanceof Error ? error.message : String(error));
|
||||||
"Failed to access camera. Please check your camera permissions.",
|
|
||||||
"Camera 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 {
|
try {
|
||||||
// Here you would implement QR code scanning from the image
|
this.addCameraState('checking_permissions');
|
||||||
// For example, using jsQR:
|
const capabilities = this.platformService.getCapabilities();
|
||||||
// const image = new Image();
|
if (!capabilities.hasCamera) {
|
||||||
// image.src = imageDataUrl;
|
this.addCameraState('no_camera_capability');
|
||||||
// image.onload = () => {
|
return { camera: 'denied' as CameraPermissionState };
|
||||||
// const canvas = document.createElement('canvas');
|
}
|
||||||
// const context = canvas.getContext('2d');
|
|
||||||
// canvas.width = image.width;
|
const permissionStatus = await Camera.checkPermissions();
|
||||||
// canvas.height = image.height;
|
this.addCameraState('permission_status_checked');
|
||||||
// context.drawImage(image, 0, 0);
|
|
||||||
// const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
if (permissionStatus.camera === 'prompt') {
|
||||||
// const code = jsQR(imageData.data, imageData.width, imageData.height);
|
this.addCameraState('requesting_permission');
|
||||||
// if (code) {
|
const requestResult = await Camera.requestPermissions();
|
||||||
// this.onScanDetect([{ rawValue: code.data }]);
|
this.addCameraState('permission_requested');
|
||||||
// }
|
return requestResult;
|
||||||
// };
|
}
|
||||||
|
|
||||||
|
return permissionStatus;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error processing image for QR code:", error);
|
this.addCameraState('permission_check_error');
|
||||||
this.danger(
|
return { camera: 'denied' as CameraPermissionState };
|
||||||
"Failed to process the image. Please try again.",
|
}
|
||||||
"Processing Error",
|
}
|
||||||
);
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newContact: Contact;
|
let newContact: Contact;
|
||||||
try {
|
try {
|
||||||
// Extract JWT from URL
|
// Extract JWT from URL
|
||||||
const jwt = getContactJwtFromJwtUrl(url);
|
const jwt = getContactJwtFromJwtUrl(url);
|
||||||
if (!jwt) {
|
if (!jwt) {
|
||||||
this.danger(
|
this.danger(
|
||||||
"Could not extract contact information from the QR code. Please try again.",
|
"Could not extract contact information from the QR code. Please try again.",
|
||||||
"Invalid QR Code",
|
"Invalid QR Code",
|
||||||
@@ -366,11 +620,11 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.danger(
|
this.danger(
|
||||||
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
|
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
|
||||||
"Invalid Data",
|
"Invalid Data",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { payload } = decodeEndorserJwt(jwt);
|
const { payload } = decodeEndorserJwt(jwt);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
this.danger(
|
this.danger(
|
||||||
"Could not decode the contact information. Please try again.",
|
"Could not decode the contact information. Please try again.",
|
||||||
@@ -388,7 +642,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newContact = {
|
newContact = {
|
||||||
did: payload.own?.did || payload.iss,
|
did: payload.own?.did || payload.iss,
|
||||||
name: payload.own?.name,
|
name: payload.own?.name,
|
||||||
nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
|
nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
|
||||||
@@ -397,15 +651,15 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
registered: payload.own?.registered,
|
registered: payload.own?.registered,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!newContact.did) {
|
if (!newContact.did) {
|
||||||
this.danger(
|
this.danger(
|
||||||
"Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
|
"Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
|
||||||
"Incomplete Contact",
|
"Incomplete Contact",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDid(newContact.did)) {
|
if (!isDid(newContact.did)) {
|
||||||
this.danger(
|
this.danger(
|
||||||
"Invalid contact identifier format. The identifier must begin with 'did:'.",
|
"Invalid contact identifier format. The identifier must begin with 'did:'.",
|
||||||
"Invalid Identifier",
|
"Invalid Identifier",
|
||||||
@@ -413,66 +667,66 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
await db.contacts.add(newContact);
|
await db.contacts.add(newContact);
|
||||||
|
|
||||||
let addedMessage;
|
let addedMessage;
|
||||||
if (this.activeDid) {
|
if (this.activeDid) {
|
||||||
await this.setVisibility(newContact, true);
|
await this.setVisibility(newContact, true);
|
||||||
newContact.seesMe = true;
|
newContact.seesMe = true;
|
||||||
addedMessage = "They were added, and your activity is visible to them.";
|
addedMessage = "They were added, and your activity is visible to them.";
|
||||||
} else {
|
} else {
|
||||||
addedMessage = "They were added.";
|
addedMessage = "They were added.";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Contact Added",
|
title: "Contact Added",
|
||||||
text: addedMessage,
|
text: addedMessage,
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.isRegistered &&
|
this.isRegistered &&
|
||||||
!this.hideRegisterPromptOnNewContact &&
|
!this.hideRegisterPromptOnNewContact &&
|
||||||
!newContact.registered
|
!newContact.registered
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "modal",
|
group: "modal",
|
||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Register",
|
title: "Register",
|
||||||
text: "Do you want to register them?",
|
text: "Do you want to register them?",
|
||||||
onCancel: async (stopAsking?: boolean) => {
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
});
|
});
|
||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNo: async (stopAsking?: boolean) => {
|
onNo: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
});
|
});
|
||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.register(newContact);
|
await this.register(newContact);
|
||||||
},
|
},
|
||||||
promptToStopAsking: true,
|
promptToStopAsking: true,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error processing QR code:", e);
|
logger.error("Error processing QR code:", e);
|
||||||
this.danger(
|
this.danger(
|
||||||
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.",
|
"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> {
|
async onCopyUrlToClipboard(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.platformService.writeToClipboard(this.qrValue);
|
await this.platformService.writeToClipboard(this.qrValue);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "toast",
|
type: "toast",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Contact URL was copied to clipboard.",
|
text: "Contact URL was copied to clipboard.",
|
||||||
},
|
},
|
||||||
2000,
|
2000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error copying to clipboard:", error);
|
logger.error("Error copying to clipboard:", error);
|
||||||
this.danger("Failed to copy 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> {
|
async onCopyDidToClipboard(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.platformService.writeToClipboard(this.activeDid);
|
await this.platformService.writeToClipboard(this.activeDid);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
|
text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error copying to clipboard:", error);
|
logger.error("Error copying to clipboard:", error);
|
||||||
this.danger("Failed to copy to clipboard", "Error");
|
this.danger("Failed to copy to clipboard", "Error");
|
||||||
@@ -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,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.*"]
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user