11 changed files with 1676 additions and 0 deletions
@ -0,0 +1,148 @@ |
|||
--- |
|||
description: |
|||
globs: |
|||
alwaysApply: true |
|||
--- |
|||
# QR Code Handling Rule |
|||
|
|||
## Architecture Overview |
|||
|
|||
The QR code scanning functionality follows a platform-agnostic design using a factory pattern that provides different implementations for web and mobile platforms. |
|||
|
|||
### Core Components |
|||
|
|||
1. **Factory Pattern** |
|||
- `QRScannerFactory` - Creates appropriate scanner instance based on platform |
|||
- Common interface `QRScannerService` implemented by all scanners |
|||
- Platform detection via Vite config flags: `__USE_QR_READER__` and `__IS_MOBILE__` |
|||
|
|||
2. **Platform-Specific Implementations** |
|||
- `CapacitorQRScanner` - Native mobile implementation |
|||
- `WebDialogQRScanner` - Web browser implementation |
|||
- `QRScannerDialog.vue` - Shared UI component |
|||
|
|||
## Mobile Implementation (Capacitor) |
|||
|
|||
### Technology Stack |
|||
- Uses `@capacitor-mlkit/barcode-scanning` plugin |
|||
- Configured in `capacitor.config.ts` |
|||
- Native camera access through platform APIs |
|||
|
|||
### Key Features |
|||
- Direct camera access via native APIs |
|||
- Optimized for mobile performance |
|||
- Supports both iOS and Android |
|||
- Real-time QR code detection |
|||
- Back camera preferred for scanning |
|||
|
|||
### Configuration |
|||
```typescript |
|||
MLKitBarcodeScanner: { |
|||
formats: ['QR_CODE'], |
|||
detectorSize: 1.0, |
|||
lensFacing: 'back', |
|||
googleBarcodeScannerModuleInstallState: true |
|||
} |
|||
``` |
|||
|
|||
### Permissions Handling |
|||
1. Check permissions via `BarcodeScanner.checkPermissions()` |
|||
2. Request permissions if needed |
|||
3. Handle permission states (granted/denied) |
|||
4. Graceful fallbacks for permission issues |
|||
|
|||
## Web Implementation |
|||
|
|||
### Technology Stack |
|||
- Uses `vue-qrcode-reader` library |
|||
- Browser's MediaDevices API |
|||
- Vue.js dialog component |
|||
|
|||
### Key Features |
|||
- Browser-based camera access |
|||
- Fallback UI for unsupported browsers |
|||
- Responsive design |
|||
- Cross-browser compatibility |
|||
- Progressive enhancement |
|||
|
|||
### Permissions Handling |
|||
1. Uses browser's permission API |
|||
2. MediaDevices API for camera access |
|||
3. Handles secure context requirements |
|||
4. Provides user feedback for permission states |
|||
|
|||
## Shared Features |
|||
|
|||
### Error Handling |
|||
1. Permission denied scenarios |
|||
2. Device compatibility checks |
|||
3. Camera access failures |
|||
4. QR code validation |
|||
5. Network connectivity issues |
|||
|
|||
### User Experience |
|||
1. Clear feedback during scanning |
|||
2. Loading states |
|||
3. Error messages |
|||
4. Success confirmations |
|||
5. Camera preview |
|||
|
|||
### Security |
|||
1. HTTPS requirement for web |
|||
2. Permission validation |
|||
3. Data validation |
|||
4. Safe error handling |
|||
|
|||
## Usage Guidelines |
|||
|
|||
### Platform Detection |
|||
```typescript |
|||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { |
|||
// Use native scanner |
|||
} else if (__USE_QR_READER__) { |
|||
// Use web scanner |
|||
} |
|||
``` |
|||
|
|||
### Implementation Example |
|||
```typescript |
|||
const scanner = QRScannerFactory.getInstance(); |
|||
await scanner.checkPermissions(); |
|||
await scanner.startScan(); |
|||
scanner.addListener({ |
|||
onScan: (result) => { |
|||
// Handle scan result |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Best Practices |
|||
1. Always check permissions before starting scan |
|||
2. Clean up resources after scanning |
|||
3. Handle all error cases |
|||
4. Provide clear user feedback |
|||
5. Test on multiple devices/browsers |
|||
|
|||
## Platform-Specific Notes |
|||
|
|||
### Mobile (Capacitor) |
|||
1. Use native camera API when available |
|||
2. Handle device rotation |
|||
3. Support both front/back cameras |
|||
4. Manage system permissions properly |
|||
5. Handle app lifecycle events |
|||
|
|||
### Web |
|||
1. Check browser compatibility |
|||
2. Handle secure context requirement |
|||
3. Manage memory usage |
|||
4. Clean up MediaStream |
|||
5. Handle tab visibility changes |
|||
|
|||
## Testing Requirements |
|||
|
|||
1. Test on multiple devices |
|||
2. Verify permission flows |
|||
3. Check error handling |
|||
4. Validate cleanup |
|||
5. Verify cross-platform behavior |
@ -0,0 +1,568 @@ |
|||
--- |
|||
description: |
|||
globs: |
|||
alwaysApply: true |
|||
--- |
|||
# QR Code Implementation Guide |
|||
|
|||
## Directory Structure |
|||
|
|||
``` |
|||
src/ |
|||
├── components/ |
|||
│ └── QRScanner/ |
|||
│ ├── types.ts |
|||
│ ├── factory.ts |
|||
│ ├── CapacitorScanner.ts |
|||
│ ├── WebDialogScanner.ts |
|||
│ └── QRScannerDialog.vue |
|||
├── services/ |
|||
│ └── QRScanner/ |
|||
│ ├── types.ts |
|||
│ ├── QRScannerFactory.ts |
|||
│ ├── CapacitorQRScanner.ts |
|||
│ └── WebDialogQRScanner.ts |
|||
``` |
|||
|
|||
## Core Interfaces |
|||
|
|||
```typescript |
|||
// types.ts |
|||
export interface ScanListener { |
|||
onScan: (result: string) => void; |
|||
onError?: (error: Error) => void; |
|||
} |
|||
|
|||
export interface QRScannerService { |
|||
checkPermissions(): Promise<boolean>; |
|||
requestPermissions(): Promise<boolean>; |
|||
isSupported(): Promise<boolean>; |
|||
startScan(): Promise<void>; |
|||
stopScan(): Promise<void>; |
|||
addListener(listener: ScanListener): void; |
|||
cleanup(): Promise<void>; |
|||
} |
|||
``` |
|||
|
|||
## Configuration Files |
|||
|
|||
### Vite Configuration |
|||
```typescript |
|||
// vite.config.ts |
|||
export default defineConfig({ |
|||
define: { |
|||
__USE_QR_READER__: JSON.stringify(!isMobile), |
|||
__IS_MOBILE__: JSON.stringify(isMobile), |
|||
}, |
|||
build: { |
|||
rollupOptions: { |
|||
external: isMobile ? ['vue-qrcode-reader'] : [], |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Capacitor Configuration |
|||
```typescript |
|||
// capacitor.config.ts |
|||
const config: CapacitorConfig = { |
|||
plugins: { |
|||
MLKitBarcodeScanner: { |
|||
formats: ['QR_CODE'], |
|||
detectorSize: 1.0, |
|||
lensFacing: 'back', |
|||
googleBarcodeScannerModuleInstallState: true |
|||
} |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
## Implementation Steps |
|||
|
|||
1. **Install Dependencies** |
|||
```bash |
|||
npm install @capacitor-mlkit/barcode-scanning vue-qrcode-reader |
|||
``` |
|||
|
|||
2. **Create Core Types** |
|||
Create the interface files as shown above. |
|||
|
|||
3. **Implement Factory** |
|||
```typescript |
|||
// QRScannerFactory.ts |
|||
export class QRScannerFactory { |
|||
private static instance: QRScannerService | null = null; |
|||
|
|||
static getInstance(): QRScannerService { |
|||
if (!this.instance) { |
|||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { |
|||
this.instance = new CapacitorQRScanner(); |
|||
} else if (__USE_QR_READER__) { |
|||
this.instance = new WebDialogQRScanner(); |
|||
} else { |
|||
throw new Error('No QR scanner implementation available'); |
|||
} |
|||
} |
|||
return this.instance; |
|||
} |
|||
|
|||
static async cleanup() { |
|||
if (this.instance) { |
|||
await this.instance.cleanup(); |
|||
this.instance = null; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
4. **Implement Mobile Scanner** |
|||
```typescript |
|||
// CapacitorQRScanner.ts |
|||
export class CapacitorQRScanner implements QRScannerService { |
|||
private scanListener: ScanListener | null = null; |
|||
private isScanning = false; |
|||
private listenerHandles: Array<() => Promise<void>> = []; |
|||
|
|||
async checkPermissions() { |
|||
try { |
|||
const { camera } = await BarcodeScanner.checkPermissions(); |
|||
return camera === 'granted'; |
|||
} catch (error) { |
|||
logger.error('Error checking camera permissions:', error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// Implement other interface methods... |
|||
} |
|||
``` |
|||
|
|||
5. **Implement Web Scanner** |
|||
```typescript |
|||
// WebDialogQRScanner.ts |
|||
export class WebDialogQRScanner implements QRScannerService { |
|||
private dialogInstance: App | null = null; |
|||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null; |
|||
private scanListener: ScanListener | null = null; |
|||
async checkPermissions(): Promise<boolean> { |
|||
try { |
|||
const permissions = await navigator.permissions.query({ |
|||
name: 'camera' as PermissionName |
|||
}); |
|||
return permissions.state === 'granted'; |
|||
} catch (error) { |
|||
logger.error('Error checking camera permissions:', error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// Implement other interface methods... |
|||
} |
|||
``` |
|||
|
|||
6. **Create Dialog Component** |
|||
```vue |
|||
<!-- QRScannerDialog.vue --> |
|||
<template> |
|||
<div v-if="visible" class="dialog-overlay z-[60]"> |
|||
<div class="dialog relative"> |
|||
<!-- Dialog content --> |
|||
<div v-if="useQRReader"> |
|||
<qrcode-stream |
|||
class="w-full max-w-lg mx-auto" |
|||
@detect="onScanDetect" |
|||
@error="onScanError" |
|||
/> |
|||
</div> |
|||
<div v-else> |
|||
<!-- Mobile camera button --> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
@Component({ |
|||
components: { QrcodeStream } |
|||
}) |
|||
export default class QRScannerDialog extends Vue { |
|||
// Implementation... |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## Usage Example |
|||
|
|||
```typescript |
|||
// In your component |
|||
async function scanQRCode() { |
|||
const scanner = QRScannerFactory.getInstance(); |
|||
|
|||
if (!(await scanner.checkPermissions())) { |
|||
const granted = await scanner.requestPermissions(); |
|||
if (!granted) { |
|||
throw new Error('Camera permission denied'); |
|||
} |
|||
} |
|||
|
|||
scanner.addListener({ |
|||
onScan: (result) => { |
|||
console.log('Scanned:', result); |
|||
}, |
|||
onError: (error) => { |
|||
console.error('Scan error:', error); |
|||
} |
|||
}); |
|||
|
|||
await scanner.startScan(); |
|||
} |
|||
|
|||
// Cleanup when done |
|||
onUnmounted(() => { |
|||
QRScannerFactory.cleanup(); |
|||
}); |
|||
``` |
|||
|
|||
## Platform-Specific Notes |
|||
|
|||
### Mobile (Capacitor) |
|||
- Uses MLKit for optimal performance |
|||
- Handles native permissions |
|||
- Supports both iOS and Android |
|||
- Uses back camera by default |
|||
- Handles device rotation |
|||
|
|||
### Web |
|||
- Uses MediaDevices API |
|||
- Requires HTTPS for camera access |
|||
- Handles browser compatibility |
|||
- Manages memory and resources |
|||
- Provides fallback UI |
|||
|
|||
## Testing |
|||
|
|||
1. **Unit Tests** |
|||
- Test factory pattern |
|||
- Test platform detection |
|||
- Test error handling |
|||
- Test cleanup procedures |
|||
|
|||
2. **Integration Tests** |
|||
- Test permission flows |
|||
- Test camera access |
|||
- Test QR code detection |
|||
- Test cross-platform behavior |
|||
|
|||
3. **E2E Tests** |
|||
- Test full scanning workflow |
|||
- Test UI feedback |
|||
- Test error scenarios |
|||
- Test platform differences |
|||
|
|||
## Common Issues and Solutions |
|||
|
|||
1. **Permission Handling** |
|||
- Always check permissions first |
|||
- Provide clear user feedback |
|||
- Handle denial gracefully |
|||
- Implement retry logic |
|||
|
|||
2. **Resource Management** |
|||
- Clean up after scanning |
|||
- Handle component unmounting |
|||
- Release camera resources |
|||
- Clear event listeners |
|||
|
|||
3. **Error Handling** |
|||
- Log errors appropriately |
|||
- Provide user feedback |
|||
- Implement fallbacks |
|||
- Handle edge cases |
|||
|
|||
4. **Performance** |
|||
- Optimize camera preview |
|||
- Handle memory usage |
|||
- Manage battery impact |
|||
- Consider device capabilities |
|||
|
|||
# QR Code Implementation Guide |
|||
|
|||
## Directory Structure |
|||
|
|||
``` |
|||
src/ |
|||
├── components/ |
|||
│ └── QRScanner/ |
|||
│ ├── types.ts |
|||
│ ├── factory.ts |
|||
│ ├── CapacitorScanner.ts |
|||
│ ├── WebDialogScanner.ts |
|||
│ └── QRScannerDialog.vue |
|||
├── services/ |
|||
│ └── QRScanner/ |
|||
│ ├── types.ts |
|||
│ ├── QRScannerFactory.ts |
|||
│ ├── CapacitorQRScanner.ts |
|||
│ └── WebDialogQRScanner.ts |
|||
``` |
|||
|
|||
## Core Interfaces |
|||
|
|||
```typescript |
|||
// types.ts |
|||
export interface ScanListener { |
|||
onScan: (result: string) => void; |
|||
onError?: (error: Error) => void; |
|||
} |
|||
|
|||
export interface QRScannerService { |
|||
checkPermissions(): Promise<boolean>; |
|||
requestPermissions(): Promise<boolean>; |
|||
isSupported(): Promise<boolean>; |
|||
startScan(): Promise<void>; |
|||
stopScan(): Promise<void>; |
|||
addListener(listener: ScanListener): void; |
|||
cleanup(): Promise<void>; |
|||
} |
|||
``` |
|||
|
|||
## Configuration Files |
|||
|
|||
### Vite Configuration |
|||
```typescript |
|||
// vite.config.ts |
|||
export default defineConfig({ |
|||
define: { |
|||
__USE_QR_READER__: JSON.stringify(!isMobile), |
|||
__IS_MOBILE__: JSON.stringify(isMobile), |
|||
}, |
|||
build: { |
|||
rollupOptions: { |
|||
external: isMobile ? ['vue-qrcode-reader'] : [], |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Capacitor Configuration |
|||
```typescript |
|||
// capacitor.config.ts |
|||
const config: CapacitorConfig = { |
|||
plugins: { |
|||
MLKitBarcodeScanner: { |
|||
formats: ['QR_CODE'], |
|||
detectorSize: 1.0, |
|||
lensFacing: 'back', |
|||
googleBarcodeScannerModuleInstallState: true |
|||
} |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
## Implementation Steps |
|||
|
|||
1. **Install Dependencies** |
|||
```bash |
|||
npm install @capacitor-mlkit/barcode-scanning vue-qrcode-reader |
|||
``` |
|||
|
|||
2. **Create Core Types** |
|||
Create the interface files as shown above. |
|||
|
|||
3. **Implement Factory** |
|||
```typescript |
|||
// QRScannerFactory.ts |
|||
export class QRScannerFactory { |
|||
private static instance: QRScannerService | null = null; |
|||
|
|||
static getInstance(): QRScannerService { |
|||
if (!this.instance) { |
|||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { |
|||
this.instance = new CapacitorQRScanner(); |
|||
} else if (__USE_QR_READER__) { |
|||
this.instance = new WebDialogQRScanner(); |
|||
} else { |
|||
throw new Error('No QR scanner implementation available'); |
|||
} |
|||
} |
|||
return this.instance; |
|||
} |
|||
|
|||
static async cleanup() { |
|||
if (this.instance) { |
|||
await this.instance.cleanup(); |
|||
this.instance = null; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
4. **Implement Mobile Scanner** |
|||
```typescript |
|||
// CapacitorQRScanner.ts |
|||
export class CapacitorQRScanner implements QRScannerService { |
|||
private scanListener: ScanListener | null = null; |
|||
private isScanning = false; |
|||
private listenerHandles: Array<() => Promise<void>> = []; |
|||
|
|||
async checkPermissions() { |
|||
try { |
|||
const { camera } = await BarcodeScanner.checkPermissions(); |
|||
return camera === 'granted'; |
|||
} catch (error) { |
|||
logger.error('Error checking camera permissions:', error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// Implement other interface methods... |
|||
} |
|||
``` |
|||
|
|||
5. **Implement Web Scanner** |
|||
```typescript |
|||
// WebDialogQRScanner.ts |
|||
export class WebDialogQRScanner implements QRScannerService { |
|||
private dialogInstance: App | null = null; |
|||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null; |
|||
private scanListener: ScanListener | null = null; |
|||
async checkPermissions(): Promise<boolean> { |
|||
try { |
|||
const permissions = await navigator.permissions.query({ |
|||
name: 'camera' as PermissionName |
|||
}); |
|||
return permissions.state === 'granted'; |
|||
} catch (error) { |
|||
logger.error('Error checking camera permissions:', error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// Implement other interface methods... |
|||
} |
|||
``` |
|||
|
|||
6. **Create Dialog Component** |
|||
```vue |
|||
<!-- QRScannerDialog.vue --> |
|||
<template> |
|||
<div v-if="visible" class="dialog-overlay z-[60]"> |
|||
<div class="dialog relative"> |
|||
<!-- Dialog content --> |
|||
<div v-if="useQRReader"> |
|||
<qrcode-stream |
|||
class="w-full max-w-lg mx-auto" |
|||
@detect="onScanDetect" |
|||
@error="onScanError" |
|||
/> |
|||
</div> |
|||
<div v-else> |
|||
<!-- Mobile camera button --> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
@Component({ |
|||
components: { QrcodeStream } |
|||
}) |
|||
export default class QRScannerDialog extends Vue { |
|||
// Implementation... |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## Usage Example |
|||
|
|||
```typescript |
|||
// In your component |
|||
async function scanQRCode() { |
|||
const scanner = QRScannerFactory.getInstance(); |
|||
|
|||
if (!(await scanner.checkPermissions())) { |
|||
const granted = await scanner.requestPermissions(); |
|||
if (!granted) { |
|||
throw new Error('Camera permission denied'); |
|||
} |
|||
} |
|||
|
|||
scanner.addListener({ |
|||
onScan: (result) => { |
|||
console.log('Scanned:', result); |
|||
}, |
|||
onError: (error) => { |
|||
console.error('Scan error:', error); |
|||
} |
|||
}); |
|||
|
|||
await scanner.startScan(); |
|||
} |
|||
|
|||
// Cleanup when done |
|||
onUnmounted(() => { |
|||
QRScannerFactory.cleanup(); |
|||
}); |
|||
``` |
|||
|
|||
## Platform-Specific Notes |
|||
|
|||
### Mobile (Capacitor) |
|||
- Uses MLKit for optimal performance |
|||
- Handles native permissions |
|||
- Supports both iOS and Android |
|||
- Uses back camera by default |
|||
- Handles device rotation |
|||
|
|||
### Web |
|||
- Uses MediaDevices API |
|||
- Requires HTTPS for camera access |
|||
- Handles browser compatibility |
|||
- Manages memory and resources |
|||
- Provides fallback UI |
|||
|
|||
## Testing |
|||
|
|||
1. **Unit Tests** |
|||
- Test factory pattern |
|||
- Test platform detection |
|||
- Test error handling |
|||
- Test cleanup procedures |
|||
|
|||
2. **Integration Tests** |
|||
- Test permission flows |
|||
- Test camera access |
|||
- Test QR code detection |
|||
- Test cross-platform behavior |
|||
|
|||
3. **E2E Tests** |
|||
- Test full scanning workflow |
|||
- Test UI feedback |
|||
- Test error scenarios |
|||
- Test platform differences |
|||
|
|||
## Common Issues and Solutions |
|||
|
|||
1. **Permission Handling** |
|||
- Always check permissions first |
|||
- Provide clear user feedback |
|||
- Handle denial gracefully |
|||
- Implement retry logic |
|||
|
|||
2. **Resource Management** |
|||
- Clean up after scanning |
|||
- Handle component unmounting |
|||
- Release camera resources |
|||
- Clear event listeners |
|||
|
|||
3. **Error Handling** |
|||
- Log errors appropriately |
|||
- Provide user feedback |
|||
- Implement fallbacks |
|||
- Handle edge cases |
|||
|
|||
4. **Performance** |
|||
- Optimize camera preview |
|||
- Handle memory usage |
|||
- Manage battery impact |
|||
- Consider device capabilities |
@ -0,0 +1,276 @@ |
|||
--- |
|||
description: |
|||
globs: |
|||
alwaysApply: true |
|||
--- |
|||
--- |
|||
description: |
|||
globs: |
|||
alwaysApply: true |
|||
--- |
|||
# Time Safari Context |
|||
|
|||
## Project Overview |
|||
|
|||
Time Safari is an application designed to foster community building through gifts, gratitude, and collaborative projects. The app should make it extremely easy and intuitive for users of any age and capability to recognize contributions, build trust networks, and organize collective action. It is built on services that preserve privacy and data sovereignty. |
|||
|
|||
The ultimate goals of Time Safari are two-fold: |
|||
|
|||
1. **Connect** Make it easy, rewarding, and non-threatening for people to connect with others who have similar interests, and to initiate activities together. This helps people accomplish and learn from other individuals in less-structured environments; moreover, it helps them discover who they want to continue to support and with whom they want to maintain relationships. |
|||
|
|||
2. **Reveal** Widely advertise the great support and rewards that are being given and accepted freely, especially non-monetary ones. Using visuals and text, display the kind of impact that gifts are making in the lives of others. Also show useful and engaging reports of project statistics and personal accomplishments. |
|||
|
|||
|
|||
## Core Approaches |
|||
|
|||
Time Safari should help everyday users build meaningful connections and organize collective efforts by: |
|||
|
|||
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts and contributions people give to each other and their communities. |
|||
|
|||
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask for or propose help on projects and interests that matter to them. |
|||
|
|||
3. **Building Trust Networks**: Enabling users to maintain their network and activity visibility. Developing reputation through verified contributions and references, which can be selectively shown to others outside the network. |
|||
|
|||
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with explicitly authorized contacts, allowing private individuals including children to participate safely. |
|||
|
|||
5. **Engaging Content**: Displaying people's records in compelling stories, and highlighting those projects that are lifting people's lives long-term, both in physical support and in emotional-spiritual-creative thriving. |
|||
|
|||
|
|||
## Technical Foundation |
|||
|
|||
This application is built on a privacy-preserving claims architecture (via endorser.ch) with these key characteristics: |
|||
|
|||
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private key pairs stored on their devices |
|||
- **Cryptographic Verification**: All claims and confirmations are cryptographically signed |
|||
- **User-Controlled Visibility**: Users explicitly control who can see their identifiers and data |
|||
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification and integrity |
|||
- **Native and Web App**: Works on Capacitor (iOS, Android), Desktop (Electron and CEFPython), and web browsers |
|||
|
|||
## User Journey |
|||
|
|||
The typical progression of usage follows these stages: |
|||
|
|||
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude for gifts received, building a foundation of acknowledgment. |
|||
|
|||
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect with others who share similar interests. |
|||
|
|||
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute proposed projects, moving from ideas to action. |
|||
|
|||
## Context for LLM Development |
|||
|
|||
When developing new functionality for Time Safari, consider these design principles: |
|||
|
|||
1. **Accessibility First**: Features should be usable by non-technical users with minimal learning curve. |
|||
|
|||
2. **Privacy by Design**: All features must respect user privacy and data sovereignty. |
|||
|
|||
3. **Progressive Enhancement**: Core functionality should work across all devices, with richer experiences where supported. |
|||
|
|||
4. **Voluntary Collaboration**: The system should enable but never coerce participation. |
|||
|
|||
5. **Trust Building**: Features should help build verifiable trust between users. |
|||
|
|||
6. **Network Effects**: Consider how features scale as more users join the platform. |
|||
|
|||
7. **Low Resource Requirements**: The system should be lightweight enough to run on inexpensive devices users already own. |
|||
|
|||
## Use Cases to Support |
|||
|
|||
LLM development should focus on enhancing these key use cases: |
|||
|
|||
1. **Community Building**: Tools that help people find others with shared interests and values. |
|||
|
|||
2. **Project Coordination**: Features that make it easy to propose collaborative projects and to submit suggestions and offers to existing ones. |
|||
|
|||
3. **Reputation Building**: Methods for users to showcase their contributions and reliability, in contexts where they explicitly reveal that information. |
|||
|
|||
4. **Governance Experimentation**: Features that facilitate decision-making and collective governance. |
|||
|
|||
## Constraints |
|||
|
|||
When developing new features, be mindful of these constraints: |
|||
|
|||
1. **Privacy Preservation**: User identifiers must remain private except when explicitly shared. |
|||
|
|||
2. **Platform Limitations**: Features must work within the constraints of the target app platforms, while aiming to leverage the best platform technology available. |
|||
|
|||
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch API capabilities. |
|||
|
|||
4. **Performance on Low-End Devices**: The application should remain performant on older/simpler devices. |
|||
|
|||
5. **Offline-First When Possible**: Key functionality should work offline when feasible. |
|||
|
|||
## Project Technologies |
|||
|
|||
- Typescript using ES6 classes using vue-facing-decorator |
|||
- TailwindCSS |
|||
- Vite Build Tool |
|||
- Playwright E2E testing |
|||
- IndexDB |
|||
- Camera, Image uploads, QR Code reader, ... |
|||
|
|||
## Mobile Features |
|||
|
|||
- Deep Linking |
|||
- Local Notifications via a custom Capacitor plugin |
|||
|
|||
## Project Architecture |
|||
|
|||
- The application must work on web browser, PWA (Progressive Web Application), desktop via Electron, and mobile via Capacitor |
|||
- Building for each platform is managed via Vite |
|||
|
|||
## Core Development Principles |
|||
|
|||
### DRY development |
|||
- **Code Reuse** |
|||
- Extract common functionality into utility functions |
|||
- Create reusable components for UI patterns |
|||
- Implement service classes for shared business logic |
|||
- Use mixins for cross-cutting concerns |
|||
- Leverage TypeScript interfaces for shared type definitions |
|||
|
|||
- **Component Patterns** |
|||
- Create base components for common UI elements |
|||
- Implement higher-order components for shared behavior |
|||
- Use slot patterns for flexible component composition |
|||
- Create composable services for business logic |
|||
- Implement factory patterns for component creation |
|||
|
|||
- **State Management** |
|||
- Centralize state in Pinia stores |
|||
- Use computed properties for derived state |
|||
- Implement shared state selectors |
|||
- Create reusable state mutations |
|||
- Use action creators for common operations |
|||
|
|||
- **Error Handling** |
|||
- Implement centralized error handling |
|||
- Create reusable error components |
|||
- Use error boundary components |
|||
- Implement consistent error logging |
|||
- Create error type definitions |
|||
|
|||
- **Type Definitions** |
|||
- Create shared interfaces for common data structures |
|||
- Use type aliases for complex types |
|||
- Implement generic types for reusable components |
|||
- Create utility types for common patterns |
|||
- Use discriminated unions for state management |
|||
|
|||
- **API Integration** |
|||
- Create reusable API client classes |
|||
- Implement request/response interceptors |
|||
- Use consistent error handling patterns |
|||
- Create type-safe API endpoints |
|||
- Implement caching strategies |
|||
|
|||
- **Platform Services** |
|||
- Abstract platform-specific code behind interfaces |
|||
- Create platform-agnostic service layers |
|||
- Implement feature detection |
|||
- Use dependency injection for services |
|||
- Create service factories |
|||
|
|||
- **Testing** |
|||
- Create reusable test utilities |
|||
- Implement test factories |
|||
- Use shared test configurations |
|||
- Create reusable test helpers |
|||
- Implement consistent test patterns |
|||
|
|||
### SOLID Principles |
|||
- **Single Responsibility**: Each class/component should have only one reason to change |
|||
- Components should focus on one specific feature (e.g., QR scanning, DID management) |
|||
- Services should handle one type of functionality (e.g., platform services, crypto services) |
|||
- Utilities should provide focused helper functions |
|||
|
|||
- **Open/Closed**: Software entities should be open for extension but closed for modification |
|||
- Use interfaces for service definitions |
|||
- Implement plugin architecture for platform-specific features |
|||
- Allow component behavior extension through props and events |
|||
|
|||
- **Liskov Substitution**: Objects should be replaceable with their subtypes |
|||
- Platform services should work consistently across web/mobile |
|||
- Authentication providers should be interchangeable |
|||
- Storage implementations should be swappable |
|||
|
|||
- **Interface Segregation**: Clients shouldn't depend on interfaces they don't use |
|||
- Break down large service interfaces into smaller, focused ones |
|||
- Component props should be minimal and purposeful |
|||
- Event emissions should be specific and targeted |
|||
|
|||
- **Dependency Inversion**: High-level modules shouldn't depend on low-level modules |
|||
- Use dependency injection for services |
|||
- Abstract platform-specific code behind interfaces |
|||
- Implement factory patterns for component creation |
|||
|
|||
### Law of Demeter |
|||
- Components should only communicate with immediate dependencies |
|||
- Avoid chaining method calls (e.g., `this.service.getUser().getProfile().getName()`) |
|||
- Use mediator patterns for complex component interactions |
|||
- Implement facade patterns for subsystem access |
|||
- Keep component communication through defined events and props |
|||
|
|||
### Composition over Inheritance |
|||
- Prefer building components through composition |
|||
- Use mixins for shared functionality |
|||
- Implement feature toggles through props |
|||
- Create higher-order components for common patterns |
|||
- Use service composition for complex features |
|||
|
|||
### Interface Segregation |
|||
- Define clear interfaces for services |
|||
- Keep component APIs minimal and focused |
|||
- Split large interfaces into smaller, specific ones |
|||
- Use TypeScript interfaces for type definitions |
|||
- Implement role-based interfaces for different use cases |
|||
|
|||
### Fail Fast |
|||
- Validate inputs early in the process |
|||
- Use TypeScript strict mode |
|||
- Implement comprehensive error handling |
|||
- Add runtime checks for critical operations |
|||
- Use assertions for development-time validation |
|||
|
|||
### Principle of Least Astonishment |
|||
- Follow Vue.js conventions consistently |
|||
- Use familiar naming patterns |
|||
- Implement predictable component behaviors |
|||
- Maintain consistent error handling |
|||
- Keep UI interactions intuitive |
|||
|
|||
### Information Hiding |
|||
- Encapsulate implementation details |
|||
- Use private class members |
|||
- Implement proper access modifiers |
|||
- Hide complex logic behind simple interfaces |
|||
- Use TypeScript's access modifiers effectively |
|||
|
|||
### Single Source of Truth |
|||
- Use Pinia for state management |
|||
- Maintain one source for user data |
|||
- Centralize configuration management |
|||
- Use computed properties for derived state |
|||
- Implement proper state synchronization |
|||
|
|||
### Principle of Least Privilege |
|||
- Implement proper access control |
|||
- Use minimal required permissions |
|||
- Follow privacy-by-design principles |
|||
- Restrict component access to necessary data |
|||
- Implement proper authentication/authorization |
|||
|
|||
### Continuous Integration/Continuous Deployment (CI/CD) |
|||
- Automated testing on every commit |
|||
- Consistent build process across platforms |
|||
- Automated deployment pipelines |
|||
- Quality gates for code merging |
|||
- Environment-specific configurations |
|||
|
|||
This expanded documentation provides: |
|||
1. Clear principles for development |
|||
2. Practical implementation guidelines |
|||
3. Real-world examples |
|||
4. TypeScript integration |
|||
5. Best practices for Time Safari |
|||
|
@ -0,0 +1,28 @@ |
|||
{ |
|||
"project_info": { |
|||
"project_number": "123456789000", |
|||
"project_id": "timesafari-app", |
|||
"storage_bucket": "timesafari-app.appspot.com" |
|||
}, |
|||
"client": [ |
|||
{ |
|||
"client_info": { |
|||
"mobilesdk_app_id": "1:123456789000:android:1234567890abcdef", |
|||
"android_client_info": { |
|||
"package_name": "app.timesafari.app" |
|||
} |
|||
}, |
|||
"oauth_client": [], |
|||
"api_key": [ |
|||
{ |
|||
"current_key": "AIzaSyDummyKeyForBuildPurposesOnly12345" |
|||
} |
|||
], |
|||
"services": { |
|||
"appinvite_service": { |
|||
"other_platform_oauth_client": [] |
|||
} |
|||
} |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,156 @@ |
|||
## Build Configuration |
|||
|
|||
### Common Vite Configuration |
|||
```typescript |
|||
// vite.config.common.mts |
|||
export async function createBuildConfig(mode: string) { |
|||
const isCapacitor = mode === "capacitor"; |
|||
|
|||
return defineConfig({ |
|||
build: { |
|||
rollupOptions: { |
|||
output: { |
|||
manualChunks: { |
|||
'vue-vendor': ['vue', 'vue-router', 'vue-facing-decorator'] |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
define: { |
|||
__USE_QR_READER__: JSON.stringify(!isCapacitor), |
|||
__IS_MOBILE__: JSON.stringify(isCapacitor), |
|||
}, |
|||
optimizeDeps: { |
|||
include: [ |
|||
'@capacitor-mlkit/barcode-scanning', |
|||
'vue-qrcode-reader' |
|||
] |
|||
}, |
|||
resolve: { |
|||
alias: { |
|||
'@capacitor/app': path.resolve(__dirname, 'node_modules/@capacitor/app') |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
### Web-Specific Configuration |
|||
```typescript |
|||
// vite.config.web.mts |
|||
import { defineConfig, mergeConfig } from "vite"; |
|||
import { createBuildConfig } from "./vite.config.common.mts"; |
|||
|
|||
export default defineConfig(async () => { |
|||
const baseConfig = await createBuildConfig('web'); |
|||
|
|||
return mergeConfig(baseConfig, { |
|||
define: { |
|||
__USE_QR_READER__: true, |
|||
__IS_MOBILE__: false, |
|||
} |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
### Capacitor-Specific Configuration |
|||
```typescript |
|||
// vite.config.capacitor.mts |
|||
import { defineConfig, mergeConfig } from "vite"; |
|||
import { createBuildConfig } from "./vite.config.common.mts"; |
|||
|
|||
export default defineConfig(async () => { |
|||
const baseConfig = await createBuildConfig('capacitor'); |
|||
|
|||
return mergeConfig(baseConfig, { |
|||
define: { |
|||
__USE_QR_READER__: false, |
|||
__IS_MOBILE__: true, |
|||
}, |
|||
build: { |
|||
rollupOptions: { |
|||
external: ['vue-qrcode-reader'], // Exclude web QR reader from mobile builds |
|||
output: { |
|||
entryFileNames: '[name]-mobile.js', |
|||
chunkFileNames: '[name]-mobile.js', |
|||
assetFileNames: '[name]-mobile.[ext]' |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
### Build Scripts |
|||
Add these scripts to your `package.json`: |
|||
```json |
|||
{ |
|||
"scripts": { |
|||
"build:web": "vite build --config vite.config.web.mts", |
|||
"build:capacitor": "vite build --config vite.config.capacitor.mts", |
|||
"build:all": "npm run build:web && npm run build:capacitor" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Environment Variables |
|||
Create a `.env` file: |
|||
```bash |
|||
# QR Scanner Configuration |
|||
VITE_QR_SCANNER_ENABLED=true |
|||
VITE_DEFAULT_CAMERA=back |
|||
``` |
|||
|
|||
### Build Process |
|||
|
|||
1. **Web Build** |
|||
```bash |
|||
npm run build:web |
|||
``` |
|||
This will: |
|||
- Include vue-qrcode-reader |
|||
- Set __USE_QR_READER__ to true |
|||
- Set __IS_MOBILE__ to false |
|||
- Build for web browsers |
|||
|
|||
2. **Capacitor Build** |
|||
```bash |
|||
npm run build:capacitor |
|||
``` |
|||
This will: |
|||
- Exclude vue-qrcode-reader |
|||
- Set __USE_QR_READER__ to false |
|||
- Set __IS_MOBILE__ to true |
|||
- Build for mobile platforms |
|||
|
|||
3. **Build Both** |
|||
```bash |
|||
npm run build:all |
|||
``` |
|||
|
|||
### Important Notes |
|||
|
|||
1. **Dependencies** |
|||
- Ensure all QR-related dependencies are properly listed in package.json |
|||
- Use exact versions to avoid compatibility issues |
|||
- Consider using peer dependencies for shared libraries |
|||
|
|||
2. **Bundle Size** |
|||
- Web build includes vue-qrcode-reader (~100KB) |
|||
- Mobile build includes @capacitor-mlkit/barcode-scanning (~50KB) |
|||
- Consider using dynamic imports for lazy loading |
|||
|
|||
3. **Platform Detection** |
|||
- Build flags determine which implementation to use |
|||
- Runtime checks provide fallback options |
|||
- Environment variables can override defaults |
|||
|
|||
4. **Performance** |
|||
- Mobile builds optimize for native performance |
|||
- Web builds include necessary polyfills |
|||
- Chunk splitting improves load times |
|||
|
|||
5. **Debugging** |
|||
- Source maps are enabled for development |
|||
- Build artifacts are properly named for identification |
|||
- Console logs help track initialization |
@ -0,0 +1,158 @@ |
|||
<!-- QRScannerDialog.vue --> |
|||
<template> |
|||
<div |
|||
v-if="visible && !isNativePlatform" |
|||
class="dialog-overlay z-[60] fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" |
|||
> |
|||
<div |
|||
class="dialog relative bg-white rounded-lg shadow-xl max-w-lg w-full mx-4" |
|||
> |
|||
<!-- Header --> |
|||
<div class="p-4 border-b border-gray-200"> |
|||
<h3 class="text-lg font-medium text-gray-900">Scan QR Code</h3> |
|||
<button |
|||
class="absolute top-4 right-4 text-gray-400 hover:text-gray-500" |
|||
aria-label="Close dialog" |
|||
@click="close" |
|||
> |
|||
<svg |
|||
class="h-6 w-6" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M6 18L18 6M6 6l12 12" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
</div> |
|||
|
|||
<!-- Scanner --> |
|||
<div class="p-4"> |
|||
<div v-if="useQRReader && !isNativePlatform" class="relative aspect-square"> |
|||
<qrcode-stream |
|||
:camera="options?.camera === 'front' ? 'user' : 'environment'" |
|||
@decode="onDecode" |
|||
@init="onInit" |
|||
/> |
|||
<div |
|||
class="absolute inset-0 border-2 border-blue-500 opacity-50 pointer-events-none" |
|||
></div> |
|||
</div> |
|||
<div v-else class="text-center py-8"> |
|||
<p class="text-gray-500"> |
|||
{{ isNativePlatform ? 'Using native camera scanner...' : 'QR code scanning is not supported in this browser.' }} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Footer --> |
|||
<div class="p-4 border-t border-gray-200"> |
|||
<p v-if="error" class="text-red-500 text-sm mb-4">{{ error }}</p> |
|||
<div class="flex justify-end"> |
|||
<button |
|||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200" |
|||
@click="close" |
|||
> |
|||
Cancel |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Prop, Vue } from "vue-facing-decorator"; |
|||
import { QrcodeStream } from "vue-qrcode-reader"; |
|||
import { QRScannerOptions } from "@/services/QRScanner/types"; |
|||
import { logger } from "@/utils/logger"; |
|||
import { Capacitor } from "@capacitor/core"; |
|||
|
|||
@Component({ |
|||
components: { |
|||
QrcodeStream, |
|||
}, |
|||
}) |
|||
export default class QRScannerDialog extends Vue { |
|||
@Prop({ type: Function, required: true }) onScan!: (result: string) => void; |
|||
@Prop({ type: Function }) onError?: (error: Error) => void; |
|||
@Prop({ type: Object }) options?: QRScannerOptions; |
|||
|
|||
visible = true; |
|||
error: string | null = null; |
|||
useQRReader = __USE_QR_READER__; |
|||
isNativePlatform = Capacitor.isNativePlatform() || __IS_MOBILE__ || Capacitor.getPlatform() === 'android' || Capacitor.getPlatform() === 'ios'; |
|||
|
|||
created() { |
|||
logger.log('QRScannerDialog platform detection:', { |
|||
capacitorNative: Capacitor.isNativePlatform(), |
|||
isMobile: __IS_MOBILE__, |
|||
platform: Capacitor.getPlatform(), |
|||
useQRReader: this.useQRReader, |
|||
isNativePlatform: this.isNativePlatform |
|||
}); |
|||
|
|||
// If on native platform, close immediately and don't initialize web scanner |
|||
if (this.isNativePlatform) { |
|||
logger.log('Closing QR dialog on native platform'); |
|||
this.$nextTick(() => this.close()); |
|||
} |
|||
} |
|||
|
|||
async onInit(promise: Promise<void>): Promise<void> { |
|||
// Don't initialize on mobile platforms |
|||
if (this.isNativePlatform) { |
|||
logger.log('Skipping web scanner initialization on native platform'); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
await promise; |
|||
this.error = null; |
|||
} catch (error) { |
|||
this.error = error instanceof Error ? error.message : String(error); |
|||
if (this.onError) { |
|||
this.onError(error instanceof Error ? error : new Error(String(error))); |
|||
} |
|||
logger.error("Error initializing QR scanner:", error); |
|||
} |
|||
} |
|||
|
|||
onDecode(result: string): void { |
|||
try { |
|||
this.onScan(result); |
|||
this.close(); |
|||
} catch (error) { |
|||
this.error = error instanceof Error ? error.message : String(error); |
|||
if (this.onError) { |
|||
this.onError(error instanceof Error ? error : new Error(String(error))); |
|||
} |
|||
logger.error("Error handling QR scan result:", error); |
|||
} |
|||
} |
|||
|
|||
async close(): Promise<void> { |
|||
this.visible = false; |
|||
await this.$nextTick(); |
|||
if (this.$el && this.$el.parentNode) { |
|||
this.$el.parentNode.removeChild(this.$el); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.dialog-overlay { |
|||
backdrop-filter: blur(4px); |
|||
} |
|||
|
|||
.qrcode-stream { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,4 @@ |
|||
/// <reference types="vite/client" />
|
|||
|
|||
declare const __USE_QR_READER__: boolean; |
|||
declare const __IS_MOBILE__: boolean; |
@ -0,0 +1,118 @@ |
|||
import { |
|||
BarcodeScanner, |
|||
BarcodeFormat, |
|||
StartScanOptions, |
|||
LensFacing, |
|||
} from "@capacitor-mlkit/barcode-scanning"; |
|||
import { QRScannerService, ScanListener, QRScannerOptions } from "./types"; |
|||
import { logger } from "@/utils/logger"; |
|||
|
|||
export class CapacitorQRScanner implements QRScannerService { |
|||
private scanListener: ScanListener | null = null; |
|||
private isScanning = false; |
|||
|
|||
async checkPermissions(): Promise<boolean> { |
|||
try { |
|||
const { camera } = await BarcodeScanner.checkPermissions(); |
|||
return camera === "granted"; |
|||
} catch (error) { |
|||
logger.error("Error checking camera permissions:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async requestPermissions(): Promise<boolean> { |
|||
try { |
|||
// First check if we already have permissions
|
|||
if (await this.checkPermissions()) { |
|||
return true; |
|||
} |
|||
|
|||
// Request permissions if we don't have them
|
|||
const { camera } = await BarcodeScanner.requestPermissions(); |
|||
return camera === "granted"; |
|||
} catch (error) { |
|||
logger.error("Error requesting camera permissions:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async isSupported(): Promise<boolean> { |
|||
try { |
|||
const { supported } = await BarcodeScanner.isSupported(); |
|||
return supported; |
|||
} catch (error) { |
|||
logger.error("Error checking scanner support:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async startScan(options?: QRScannerOptions): Promise<void> { |
|||
if (this.isScanning) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
// Ensure we have permissions before starting
|
|||
logger.log('Checking camera permissions...'); |
|||
if (!(await this.checkPermissions())) { |
|||
logger.log('Requesting camera permissions...'); |
|||
const granted = await this.requestPermissions(); |
|||
if (!granted) { |
|||
throw new Error("Camera permission denied"); |
|||
} |
|||
} |
|||
|
|||
// Check if scanning is supported
|
|||
logger.log('Checking scanner support...'); |
|||
if (!(await this.isSupported())) { |
|||
throw new Error("QR scanning not supported on this device"); |
|||
} |
|||
|
|||
logger.log('Starting MLKit scanner...'); |
|||
this.isScanning = true; |
|||
|
|||
const scanOptions: StartScanOptions = { |
|||
formats: [BarcodeFormat.QrCode], |
|||
lensFacing: |
|||
options?.camera === "front" ? LensFacing.Front : LensFacing.Back, |
|||
}; |
|||
|
|||
logger.log('Scanner options:', scanOptions); |
|||
const result = await BarcodeScanner.scan(scanOptions); |
|||
logger.log('Scan result:', result); |
|||
|
|||
if (result.barcodes.length > 0) { |
|||
this.scanListener?.onScan(result.barcodes[0].rawValue); |
|||
} |
|||
} catch (error) { |
|||
logger.error("Error during QR scan:", error); |
|||
this.scanListener?.onError?.(error as Error); |
|||
} finally { |
|||
this.isScanning = false; |
|||
} |
|||
} |
|||
|
|||
async stopScan(): Promise<void> { |
|||
if (!this.isScanning) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
await BarcodeScanner.stopScan(); |
|||
this.isScanning = false; |
|||
} catch (error) { |
|||
logger.error("Error stopping QR scan:", error); |
|||
this.scanListener?.onError?.(error as Error); |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener; |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
await this.stopScan(); |
|||
this.scanListener = null; |
|||
} |
|||
} |
@ -0,0 +1,63 @@ |
|||
import { Capacitor } from "@capacitor/core"; |
|||
import { QRScannerService } from "./types"; |
|||
import { CapacitorQRScanner } from "./CapacitorQRScanner"; |
|||
import { WebDialogQRScanner } from "./WebDialogQRScanner"; |
|||
import { logger } from "@/utils/logger"; |
|||
|
|||
/** |
|||
* Factory class for creating QR scanner instances based on platform |
|||
*/ |
|||
export class QRScannerFactory { |
|||
private static instance: QRScannerService | null = null; |
|||
|
|||
private static isNativePlatform(): boolean { |
|||
const capacitorNative = Capacitor.isNativePlatform(); |
|||
const isMobile = __IS_MOBILE__; |
|||
const platform = Capacitor.getPlatform(); |
|||
|
|||
logger.log('Platform detection:', { |
|||
capacitorNative, |
|||
isMobile, |
|||
platform, |
|||
userAgent: navigator.userAgent |
|||
}); |
|||
|
|||
// Force native scanner on Android/iOS
|
|||
if (platform === 'android' || platform === 'ios') { |
|||
return true; |
|||
} |
|||
|
|||
return capacitorNative || isMobile; |
|||
} |
|||
|
|||
/** |
|||
* Get a QR scanner instance appropriate for the current platform |
|||
*/ |
|||
static getInstance(): QRScannerService { |
|||
if (!this.instance) { |
|||
const isNative = this.isNativePlatform(); |
|||
logger.log(`Creating QR scanner for platform: ${isNative ? 'native' : 'web'}`); |
|||
|
|||
if (isNative) { |
|||
logger.log('Using native MLKit scanner'); |
|||
this.instance = new CapacitorQRScanner(); |
|||
} else if (__USE_QR_READER__) { |
|||
logger.log('Using web QR scanner'); |
|||
this.instance = new WebDialogQRScanner(); |
|||
} else { |
|||
throw new Error("No QR scanner implementation available for this platform"); |
|||
} |
|||
} |
|||
return this.instance!; // We know it's not null here
|
|||
} |
|||
|
|||
/** |
|||
* Clean up the current scanner instance |
|||
*/ |
|||
static async cleanup(): Promise<void> { |
|||
if (this.instance) { |
|||
await this.instance.cleanup(); |
|||
this.instance = null; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,108 @@ |
|||
import { createApp, App } from "vue"; |
|||
import { QRScannerService, ScanListener, QRScannerOptions } from "./types"; |
|||
import QRScannerDialog from "@/components/QRScanner/QRScannerDialog.vue"; |
|||
import { logger } from "@/utils/logger"; |
|||
|
|||
export class WebDialogQRScanner implements QRScannerService { |
|||
private dialogInstance: App | null = null; |
|||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null; |
|||
private scanListener: ScanListener | null = null; |
|||
private isScanning = false; |
|||
|
|||
constructor(private options?: QRScannerOptions) {} |
|||
|
|||
async checkPermissions(): Promise<boolean> { |
|||
try { |
|||
const permissions = await navigator.permissions.query({ |
|||
name: "camera" as PermissionName, |
|||
}); |
|||
return permissions.state === "granted"; |
|||
} catch (error) { |
|||
logger.error("Error checking camera permissions:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async requestPermissions(): Promise<boolean> { |
|||
try { |
|||
const stream = await navigator.mediaDevices.getUserMedia({ video: true }); |
|||
stream.getTracks().forEach((track) => track.stop()); |
|||
return true; |
|||
} catch (error) { |
|||
logger.error("Error requesting camera permissions:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async isSupported(): Promise<boolean> { |
|||
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); |
|||
} |
|||
|
|||
async startScan(): Promise<void> { |
|||
if (this.isScanning) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
this.isScanning = true; |
|||
|
|||
// Create and mount dialog component
|
|||
const container = document.createElement("div"); |
|||
document.body.appendChild(container); |
|||
|
|||
this.dialogInstance = createApp(QRScannerDialog, { |
|||
onScan: (result: string) => { |
|||
if (this.scanListener) { |
|||
this.scanListener.onScan(result); |
|||
} |
|||
}, |
|||
onError: (error: Error) => { |
|||
if (this.scanListener?.onError) { |
|||
this.scanListener.onError(error); |
|||
} |
|||
}, |
|||
options: this.options, |
|||
}); |
|||
|
|||
this.dialogComponent = this.dialogInstance.mount(container).$refs |
|||
.dialog as InstanceType<typeof QRScannerDialog>; |
|||
} catch (error) { |
|||
this.isScanning = false; |
|||
if (this.scanListener?.onError) { |
|||
this.scanListener.onError( |
|||
error instanceof Error ? error : new Error(String(error)), |
|||
); |
|||
} |
|||
logger.error("Error starting scan:", error); |
|||
} |
|||
} |
|||
|
|||
async stopScan(): Promise<void> { |
|||
if (!this.isScanning) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
if (this.dialogComponent) { |
|||
await this.dialogComponent.close(); |
|||
} |
|||
if (this.dialogInstance) { |
|||
this.dialogInstance.unmount(); |
|||
} |
|||
this.isScanning = false; |
|||
} catch (error) { |
|||
logger.error("Error stopping scan:", error); |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener; |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
await this.stopScan(); |
|||
this.dialogComponent = null; |
|||
this.dialogInstance = null; |
|||
this.scanListener = null; |
|||
} |
|||
} |
@ -0,0 +1,49 @@ |
|||
// QR Scanner Service Types
|
|||
|
|||
/** |
|||
* Listener interface for QR code scan events |
|||
*/ |
|||
export interface ScanListener { |
|||
/** Called when a QR code is successfully scanned */ |
|||
onScan: (result: string) => void; |
|||
/** Called when an error occurs during scanning */ |
|||
onError?: (error: Error) => void; |
|||
} |
|||
|
|||
/** |
|||
* Options for configuring the QR scanner |
|||
*/ |
|||
export interface QRScannerOptions { |
|||
/** Camera to use ('front' or 'back') */ |
|||
camera?: "front" | "back"; |
|||
/** Whether to show a preview of the camera feed */ |
|||
showPreview?: boolean; |
|||
/** Whether to play a sound on successful scan */ |
|||
playSound?: boolean; |
|||
} |
|||
|
|||
/** |
|||
* Interface for QR scanner service implementations |
|||
*/ |
|||
export interface QRScannerService { |
|||
/** Check if camera permissions are granted */ |
|||
checkPermissions(): Promise<boolean>; |
|||
|
|||
/** Request camera permissions from the user */ |
|||
requestPermissions(): Promise<boolean>; |
|||
|
|||
/** Check if QR scanning is supported on this device */ |
|||
isSupported(): Promise<boolean>; |
|||
|
|||
/** Start scanning for QR codes */ |
|||
startScan(options?: QRScannerOptions): Promise<void>; |
|||
|
|||
/** Stop scanning for QR codes */ |
|||
stopScan(): Promise<void>; |
|||
|
|||
/** Add a listener for scan events */ |
|||
addListener(listener: ScanListener): void; |
|||
|
|||
/** Clean up scanner resources */ |
|||
cleanup(): Promise<void>; |
|||
} |
Loading…
Reference in new issue