Compare commits
16 Commits
master
...
qrcode-cap
| Author | SHA1 | Date |
|---|---|---|
|
|
0c3a91e56d | 6 months ago |
|
|
3214c79dbc | 6 months ago |
|
|
e5518cd47c | 6 months ago |
|
|
62553a37aa | 6 months ago |
|
|
ea13250e5d | 6 months ago |
|
|
b79cccc591 | 6 months ago |
|
|
ff75fa5349 | 6 months ago |
|
|
56f945d29f | 6 months ago |
|
|
8a9f7d4231 | 6 months ago |
|
|
3487f49f49 | 6 months ago |
|
|
f5846cbe78 | 6 months ago |
|
|
243f6f7798 | 6 months ago |
|
|
72cf0211ce | 6 months ago |
|
|
febbd4054a | 6 months ago |
|
|
3ca82fe762 | 6 months ago |
|
|
5fea1cf530 | 6 months ago |
183 changed files with 14204 additions and 11544 deletions
@ -0,0 +1,8 @@ |
|||||
|
--- |
||||
|
description: General project rules that applies to all file types. Should be most general |
||||
|
globs: **/* |
||||
|
--- |
||||
|
- Naming Conventions: Follow clear and consistent naming conventions. |
||||
|
- Performance Optimization: Optimize code for performance. |
||||
|
- Key Conventions: Adhere to project-specific key conventions. |
||||
|
- Error Handling and Validation: implement comprehensive error handling and validation. |
||||
@ -0,0 +1,7 @@ |
|||||
|
--- |
||||
|
description: Applies general TypeScript best practices and style guidelines to all TypeScript files in the project. |
||||
|
globs: **/*.ts |
||||
|
--- |
||||
|
- You are an expert in TypeScript. |
||||
|
- TypeScript Usage: Follow TypeScript best practices for type safety and code maintainability. |
||||
|
- Syntax and Formatting: Adhere to consistent coding style and formatting guidelines for TypeScript. |
||||
@ -0,0 +1,9 @@ |
|||||
|
--- |
||||
|
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: git log --since="12 hours ago" --pretty=format:"%H - %s (%an)" -p --color --all > output.txt |
||||
|
- the output.txt is the basis of work in the last 12 hours |
||||
|
- reports should always include pending issues and next steps along with urls to commits that day |
||||
@ -0,0 +1,6 @@ |
|||||
|
--- |
||||
|
description: Apply Tailwind CSS styling conventions in all relevant files. |
||||
|
globs: **/*.tsx |
||||
|
--- |
||||
|
- You are an expert in Tailwind. |
||||
|
- UI and Styling: Use Tailwind CSS for consistent UI styling. |
||||
@ -0,0 +1,270 @@ |
|||||
|
--- |
||||
|
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 iOS, Android, 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 |
||||
|
- 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,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 |
||||
@ -1,32 +0,0 @@ |
|||||
module.exports = { |
|
||||
root: true, |
|
||||
env: { |
|
||||
node: true, |
|
||||
es2022: true, |
|
||||
}, |
|
||||
extends: [ |
|
||||
"plugin:vue/vue3-recommended", |
|
||||
"eslint:recommended", |
|
||||
"@vue/typescript/recommended", |
|
||||
"plugin:prettier/recommended" |
|
||||
], |
|
||||
// parserOptions: {
|
|
||||
// ecmaVersion: 2020,
|
|
||||
// },
|
|
||||
rules: { |
|
||||
"max-len": ["warn", { |
|
||||
code: 100, |
|
||||
ignoreComments: true, |
|
||||
ignorePattern: '^\\s*class="[^"]*"$', |
|
||||
ignoreStrings: true, |
|
||||
ignoreTemplateLiterals: true, |
|
||||
ignoreUrls: true, |
|
||||
}], |
|
||||
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn", |
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn", |
|
||||
"@typescript-eslint/no-explicit-any": "warn", |
|
||||
"@typescript-eslint/explicit-function-return-type": "off", |
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "off", |
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] |
|
||||
}, |
|
||||
}; |
|
||||
@ -0,0 +1,57 @@ |
|||||
|
{ |
||||
|
"root": true, |
||||
|
"env": { |
||||
|
"node": true, |
||||
|
"browser": true, |
||||
|
"es2022": true |
||||
|
}, |
||||
|
"extends": [ |
||||
|
"plugin:vue/vue3-recommended", |
||||
|
"eslint:recommended", |
||||
|
"@vue/typescript/recommended", |
||||
|
"plugin:prettier/recommended" |
||||
|
], |
||||
|
"parser": "vue-eslint-parser", |
||||
|
"parserOptions": { |
||||
|
"parser": "@typescript-eslint/parser", |
||||
|
"ecmaVersion": 2022, |
||||
|
"sourceType": "module", |
||||
|
"extraFileExtensions": [".vue"], |
||||
|
"ecmaFeatures": { |
||||
|
"jsx": true |
||||
|
} |
||||
|
}, |
||||
|
"plugins": [ |
||||
|
"@typescript-eslint", |
||||
|
"vue", |
||||
|
"prettier" |
||||
|
], |
||||
|
"rules": { |
||||
|
"no-console": "warn", |
||||
|
"no-debugger": "warn", |
||||
|
"@typescript-eslint/no-explicit-any": "off", |
||||
|
"vue/multi-word-component-names": "off", |
||||
|
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], |
||||
|
"@typescript-eslint/no-unnecessary-type-constraint": "off", |
||||
|
"vue/no-parsing-error": ["error", { |
||||
|
"x-invalid-end-tag": false, |
||||
|
"invalid-first-character-of-tag-name": false |
||||
|
}], |
||||
|
"vue/no-v-html": "warn", |
||||
|
"prettier/prettier": ["error", { |
||||
|
"singleQuote": true, |
||||
|
"semi": false, |
||||
|
"trailingComma": "none" |
||||
|
}] |
||||
|
}, |
||||
|
"overrides": [ |
||||
|
{ |
||||
|
"files": ["*.ts", "*.tsx", "*.mts"], |
||||
|
"parser": "@typescript-eslint/parser" |
||||
|
}, |
||||
|
{ |
||||
|
"files": ["*.js", "*.jsx", "*.mjs"], |
||||
|
"parser": "@typescript-eslint/parser" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
export default { |
||||
|
root: true, |
||||
|
env: { |
||||
|
node: true, |
||||
|
browser: true, |
||||
|
es2022: true |
||||
|
}, |
||||
|
extends: [ |
||||
|
'plugin:vue/vue3-recommended', |
||||
|
'eslint:recommended', |
||||
|
'@vue/typescript/recommended' |
||||
|
], |
||||
|
parser: 'vue-eslint-parser', |
||||
|
parserOptions: { |
||||
|
parser: { |
||||
|
'ts': '@typescript-eslint/parser', |
||||
|
'js': '@typescript-eslint/parser', |
||||
|
'<template>': 'espree' |
||||
|
}, |
||||
|
ecmaVersion: 2022, |
||||
|
sourceType: 'module', |
||||
|
extraFileExtensions: ['.vue'], |
||||
|
ecmaFeatures: { |
||||
|
jsx: true |
||||
|
} |
||||
|
}, |
||||
|
plugins: [ |
||||
|
'@typescript-eslint', |
||||
|
'vue' |
||||
|
], |
||||
|
rules: { |
||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
||||
|
'@typescript-eslint/no-explicit-any': 'off', |
||||
|
'vue/multi-word-component-names': 'off', |
||||
|
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }] |
||||
|
}, |
||||
|
overrides: [ |
||||
|
{ |
||||
|
files: ['*.ts', '*.tsx', '*.mts'], |
||||
|
parser: '@typescript-eslint/parser' |
||||
|
}, |
||||
|
{ |
||||
|
files: ['*.js', '*.jsx', '*.mjs'], |
||||
|
parser: '@typescript-eslint/parser' |
||||
|
} |
||||
|
], |
||||
|
settings: { |
||||
|
'import/resolver': { |
||||
|
node: { |
||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.mjs', '.mts'] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,322 @@ |
|||||
|
# 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 iOS, Android, 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 |
||||
|
- 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 |
||||
|
|
||||
|
|
||||
|
## Vue Component Structure |
||||
|
|
||||
|
- Use `@Options`, `@Ref`, `@Prop`, `@Emit`, and `@Watch` Typescript decorators for clear component structure |
||||
|
- Extend `Vue` class with proper type annotations for props, refs, and methods |
||||
|
- Use Tailwind utility classes for accessible and responsive design |
||||
|
- Avoid `setup()` or Composition API; use class syntax consistently |
||||
|
- Keep methods pure when possible; extract logic into utilities |
||||
|
- Ensure lifecycle methods are clearly defined inside class |
||||
|
- Use semantic HTML + Tailwind classes for styling |
||||
|
- Pinia for state management |
||||
|
|
||||
|
## Vue Facing Decorators |
||||
|
|
||||
|
- Ensure all Vue 3 components are written using TypeScript with strict type checking enabled. |
||||
|
- Always include explicit types for props, emits, and reactive properties. |
||||
|
- When using @Options, ensure it includes metadata like name, template, or styles. |
||||
|
- Use @Prop for defining props with validation and default values. |
||||
|
- Use @Emit for emitting events with proper payload typing. |
||||
|
- Use @Watch for reactive property changes, and @Ref for DOM references." |
||||
|
- Organize Vue 3 components with a clear structure: imports at the top, followed by @Options metadata, then class properties (props, refs, reactive state), lifecycle hooks, methods, and finally @Watch or @Emit handlers. |
||||
|
- Ensure all props have explicit types and optional validation. |
||||
|
- Use TypeScript interfaces or types for complex prop structures. |
||||
|
- Validate default values for props where applicable. |
||||
|
- Use lifecycle hooks (e.g., onMounted, onUnmounted) sparingly and document their purpose. |
||||
|
- Avoid side effects in lifecycle hooks unless absolutely necessary. |
||||
|
- Use @Emit for emitting events with strongly typed payloads. |
||||
|
- Ensure event names are descriptive and match the action being performed. |
||||
|
- Use ref or reactive for managing internal state. |
||||
|
- Avoid overusing reactive state for simple values. Prefer computed properties for derived state. |
||||
|
- Write unit tests for components using Vue Test Utils and Jest/Vitest. |
||||
|
- Ensure tests cover props, events, and lifecycle behavior. |
||||
|
- Avoid unnecessary re-renders by using v-once for static content and memoizing expensive computations with computed properties. |
||||
|
- Ensure components are accessible by using semantic HTML and ARIA attributes. |
||||
|
- Use scoped styles or CSS modules to encapsulate styles. |
||||
|
|
||||
|
## es6 classes |
||||
|
|
||||
|
- Use ES6 class syntax with decorators (@Options, @Prop, @Emit). |
||||
|
- Use modular imports and default exports. |
||||
|
- Use arrow functions for methods and callbacks. |
||||
|
- Use destructuring for props and state. |
||||
|
- Provide default parameters for optional props or arguments. |
||||
|
- Use template literals for dynamic strings. |
||||
|
- Use spread/rest operators for object manipulation and arguments. |
||||
|
- Use const/let appropriately for variable declarations. |
||||
|
- Use enhanced object literals for cleaner syntax. |
||||
|
- Use async/await for asynchronous operations. |
||||
|
- Add scoped styles for encapsulation. |
||||
|
- Ensure accessibility with semantic HTML and ARIA attributes. |
||||
|
|
||||
|
## Documentation |
||||
|
|
||||
|
- Include JSDoc comments for all public methods and props. |
||||
|
- Files must have comments explaing contents and workflow of file |
||||
|
- Methods and props should explain role and workflow of each |
||||
Binary file not shown.
File diff suppressed because it is too large
@ -0,0 +1,124 @@ |
|||||
|
import { |
||||
|
BarcodeScanner, |
||||
|
BarcodeFormat, |
||||
|
LensFacing, |
||||
|
ScanResult |
||||
|
} from '@capacitor-mlkit/barcode-scanning' |
||||
|
import type { QRScannerService, ScanListener } from './types' |
||||
|
import { logger } from '../../utils/logger' |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async requestPermissions() { |
||||
|
try { |
||||
|
const { camera } = await BarcodeScanner.requestPermissions() |
||||
|
return camera === 'granted' |
||||
|
} catch (error) { |
||||
|
logger.error('Error requesting camera permissions:', error) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async isSupported() { |
||||
|
try { |
||||
|
const { supported } = await BarcodeScanner.isSupported() |
||||
|
return supported |
||||
|
} catch (error) { |
||||
|
logger.error('Error checking barcode scanner support:', error) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async startScan() { |
||||
|
if (this.isScanning) { |
||||
|
logger.warn('Scanner is already active') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// First register listeners before starting scan
|
||||
|
await this.registerListeners() |
||||
|
|
||||
|
this.isScanning = true |
||||
|
await BarcodeScanner.startScan({ |
||||
|
formats: [BarcodeFormat.QrCode], |
||||
|
lensFacing: LensFacing.Back |
||||
|
}) |
||||
|
} catch (error) { |
||||
|
// Ensure cleanup on error
|
||||
|
this.isScanning = false |
||||
|
await this.removeListeners() |
||||
|
logger.error('Error starting barcode scan:', error) |
||||
|
throw error |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async registerListeners() { |
||||
|
try { |
||||
|
const handle = await BarcodeScanner.addListener( |
||||
|
'barcodesScanned', |
||||
|
async (result: ScanResult) => { |
||||
|
if (result.barcodes.length > 0 && this.scanListener) { |
||||
|
const barcode = result.barcodes[0] |
||||
|
this.scanListener.onScan(barcode.rawValue) |
||||
|
await this.stopScan() |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
this.listenerHandles.push(() => handle.remove()) |
||||
|
} catch (error) { |
||||
|
logger.error('Error registering barcode listener:', error) |
||||
|
throw error |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async removeListeners() { |
||||
|
for (const remove of this.listenerHandles) { |
||||
|
try { |
||||
|
await remove() |
||||
|
} catch (error) { |
||||
|
logger.error('Error removing listener:', error) |
||||
|
} |
||||
|
} |
||||
|
this.listenerHandles = [] |
||||
|
} |
||||
|
|
||||
|
async stopScan() { |
||||
|
if (!this.isScanning) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// First stop the scan
|
||||
|
await BarcodeScanner.stopScan() |
||||
|
} catch (error) { |
||||
|
logger.error('Error stopping barcode scan:', error) |
||||
|
} finally { |
||||
|
// Always cleanup state even if stop fails
|
||||
|
this.isScanning = false |
||||
|
await this.removeListeners() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addListener(listener: ScanListener): void { |
||||
|
this.scanListener = listener |
||||
|
} |
||||
|
|
||||
|
async cleanup(): Promise<void> { |
||||
|
await this.stopScan() |
||||
|
this.scanListener = null |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,319 @@ |
|||||
|
<template> |
||||
|
<div v-if="visible" class="dialog-overlay z-[60]"> |
||||
|
<div class="dialog relative"> |
||||
|
<div class="text-lg text-center font-light relative z-50"> |
||||
|
<div |
||||
|
id="ViewHeading" |
||||
|
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none" |
||||
|
> |
||||
|
<span v-if="state.isProcessing">{{ state.processingStatus }}</span> |
||||
|
<span v-else>Scan QR Code</span> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white cursor-pointer" |
||||
|
@click="close()" |
||||
|
> |
||||
|
<font-awesome icon="xmark" class="w-[1em]" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="mt-8"> |
||||
|
<!-- Web QR Code Scanner --> |
||||
|
<qrcode-stream |
||||
|
v-if="useQRReader" |
||||
|
class="w-full max-w-lg mx-auto" |
||||
|
@detect="onScanDetect" |
||||
|
@error="onScanError" |
||||
|
/> |
||||
|
|
||||
|
<!-- Mobile Camera Button --> |
||||
|
<div v-else class="text-center mt-4"> |
||||
|
<button |
||||
|
v-if="!state.isProcessing" |
||||
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" |
||||
|
@click="openMobileCamera" |
||||
|
> |
||||
|
Open Camera |
||||
|
</button> |
||||
|
<div v-else class="text-center"> |
||||
|
<font-awesome icon="spinner" class="fa-spin fa-3x" /> |
||||
|
<p class="mt-2"> |
||||
|
{{ state.processingDetails }} |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<p v-if="state.error" class="mt-4 text-red-500 text-center"> |
||||
|
{{ state.error }} |
||||
|
</p> |
||||
|
|
||||
|
<p class="mt-4 text-sm text-gray-600 text-center"> |
||||
|
Position the QR code within the camera view to scan |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { QrcodeStream } from 'vue-qrcode-reader' |
||||
|
import { reactive } from 'vue' |
||||
|
import { |
||||
|
BarcodeScanner, |
||||
|
type ScanResult |
||||
|
} from '@capacitor-mlkit/barcode-scanning' |
||||
|
import type { PluginListenerHandle } from '@capacitor/core' |
||||
|
import { logger } from '../../utils/logger' |
||||
|
import { NotificationIface } from '../../constants/app' |
||||
|
|
||||
|
// Declare global constants |
||||
|
declare const __USE_QR_READER__: boolean |
||||
|
declare const __IS_MOBILE__: boolean |
||||
|
|
||||
|
interface AppState { |
||||
|
isProcessing: boolean |
||||
|
processingStatus: string |
||||
|
processingDetails: string |
||||
|
error: string |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
QrcodeStream |
||||
|
} |
||||
|
}) |
||||
|
export default class QRScannerDialog extends Vue { |
||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void |
||||
|
|
||||
|
visible = false |
||||
|
private scanListener: PluginListenerHandle | null = null |
||||
|
private onScanCallback: ((result: string) => void) | null = null |
||||
|
|
||||
|
state = reactive<AppState>({ |
||||
|
isProcessing: false, |
||||
|
processingStatus: '', |
||||
|
processingDetails: '', |
||||
|
error: '' |
||||
|
}) |
||||
|
|
||||
|
async open(onScan: (result: string) => void) { |
||||
|
this.onScanCallback = onScan |
||||
|
this.visible = true |
||||
|
this.state.error = '' |
||||
|
|
||||
|
if (!this.useQRReader) { |
||||
|
// Check if barcode scanning is supported on mobile |
||||
|
try { |
||||
|
const { supported } = await BarcodeScanner.isSupported() |
||||
|
if (!supported) { |
||||
|
this.showError('Barcode scanning is not supported on this device') |
||||
|
return |
||||
|
} |
||||
|
} catch (error) { |
||||
|
this.showError('Failed to check barcode scanner support') |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
close() { |
||||
|
this.visible = false |
||||
|
this.stopScanning().catch((error) => { |
||||
|
logger.error('Error stopping scanner during close:', error) |
||||
|
}) |
||||
|
this.onScanCallback = null |
||||
|
} |
||||
|
|
||||
|
async openMobileCamera() { |
||||
|
try { |
||||
|
this.state.isProcessing = true |
||||
|
this.state.processingStatus = 'Starting camera...' |
||||
|
logger.log('Opening mobile camera - starting initialization') |
||||
|
|
||||
|
// Check current permission status |
||||
|
const status = await BarcodeScanner.checkPermissions() |
||||
|
logger.log('Camera permission status:', JSON.stringify(status, null, 2)) |
||||
|
|
||||
|
if (status.camera !== 'granted') { |
||||
|
// Request permission if not granted |
||||
|
logger.log('Requesting camera permissions...') |
||||
|
const permissionStatus = await BarcodeScanner.requestPermissions() |
||||
|
if (permissionStatus.camera !== 'granted') { |
||||
|
throw new Error('Camera permission not granted') |
||||
|
} |
||||
|
logger.log( |
||||
|
'Camera permission granted:', |
||||
|
JSON.stringify(permissionStatus, null, 2) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Remove any existing listener first |
||||
|
await this.cleanupScanListener() |
||||
|
|
||||
|
// Set up the listener before starting the scan |
||||
|
logger.log('Setting up new barcode listener') |
||||
|
this.scanListener = await BarcodeScanner.addListener( |
||||
|
'barcodesScanned', |
||||
|
async (result: ScanResult) => { |
||||
|
logger.log( |
||||
|
'Barcode scan result received:', |
||||
|
JSON.stringify(result, null, 2) |
||||
|
) |
||||
|
if (result.barcodes && result.barcodes.length > 0) { |
||||
|
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}` |
||||
|
await this.handleScanResult(result.barcodes[0].rawValue) |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
logger.log('Barcode listener setup complete') |
||||
|
|
||||
|
// Start the scanner |
||||
|
logger.log('Starting barcode scanner') |
||||
|
await BarcodeScanner.startScan() |
||||
|
logger.log('Barcode scanner started successfully') |
||||
|
|
||||
|
this.state.isProcessing = false |
||||
|
this.state.processingStatus = '' |
||||
|
} catch (error) { |
||||
|
logger.error('Failed to open camera:', error) |
||||
|
this.state.isProcessing = false |
||||
|
this.state.processingStatus = '' |
||||
|
this.showError( |
||||
|
error instanceof Error ? error.message : 'Failed to open camera' |
||||
|
) |
||||
|
|
||||
|
// Cleanup on error |
||||
|
await this.cleanupScanListener() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async handleScanResult(rawValue: string) { |
||||
|
try { |
||||
|
this.state.isProcessing = true |
||||
|
this.state.processingStatus = 'Processing QR code...' |
||||
|
this.state.processingDetails = `Scanned value: ${rawValue}` |
||||
|
|
||||
|
// Stop scanning before processing |
||||
|
await this.stopScanning() |
||||
|
|
||||
|
if (this.onScanCallback) { |
||||
|
await this.onScanCallback(rawValue) |
||||
|
// Only close after the callback is complete |
||||
|
this.close() |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error('Error handling scan result:', error) |
||||
|
this.showError('Failed to process scan result') |
||||
|
} finally { |
||||
|
this.state.isProcessing = false |
||||
|
this.state.processingStatus = '' |
||||
|
this.state.processingDetails = '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async cleanupScanListener() { |
||||
|
try { |
||||
|
if (this.scanListener) { |
||||
|
await this.scanListener.remove() |
||||
|
this.scanListener = null |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error('Error removing scan listener:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async stopScanning() { |
||||
|
try { |
||||
|
await this.cleanupScanListener() |
||||
|
|
||||
|
if (!this.useQRReader) { |
||||
|
// Stop the native scanner |
||||
|
await BarcodeScanner.stopScan() |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error('Error stopping scanner:', error) |
||||
|
throw error |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Web QR reader handlers |
||||
|
async onScanDetect(result: { rawValue: string }) { |
||||
|
await this.handleScanResult(result.rawValue) |
||||
|
} |
||||
|
|
||||
|
onScanError(error: Error) { |
||||
|
logger.error('Scan error:', error) |
||||
|
this.showError('Failed to scan QR code') |
||||
|
} |
||||
|
|
||||
|
private showError(message: string) { |
||||
|
this.state.error = message |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: 'alert', |
||||
|
type: 'danger', |
||||
|
title: 'Error', |
||||
|
text: message |
||||
|
}, |
||||
|
5000 |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
get useQRReader(): boolean { |
||||
|
return __USE_QR_READER__ |
||||
|
} |
||||
|
|
||||
|
get isMobile(): boolean { |
||||
|
return __IS_MOBILE__ |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.dialog-overlay { |
||||
|
z-index: 60; |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background-color: rgba(0, 0, 0, 0.5); |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
padding: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.dialog { |
||||
|
background-color: white; |
||||
|
padding: 1rem; |
||||
|
border-radius: 0.5rem; |
||||
|
width: 100%; |
||||
|
max-width: 700px; |
||||
|
position: relative; |
||||
|
z-index: 61; |
||||
|
} |
||||
|
|
||||
|
/* Add styles for the camera preview */ |
||||
|
.qrcode-stream { |
||||
|
position: relative; |
||||
|
z-index: 62; |
||||
|
} |
||||
|
|
||||
|
.qrcode-stream video { |
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
max-height: 70vh; |
||||
|
object-fit: contain; |
||||
|
} |
||||
|
|
||||
|
/* Ensure mobile camera elements are also properly layered */ |
||||
|
.barcode-scanner-container { |
||||
|
position: relative; |
||||
|
z-index: 62; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,74 @@ |
|||||
|
import type { QRScannerService, ScanListener } from './types' |
||||
|
import QRScannerDialog from './QRScannerDialog.vue' |
||||
|
import { createApp, type App } from 'vue' |
||||
|
import { logger } from '../../utils/logger' |
||||
|
|
||||
|
// Import platform-specific flags from Vite config
|
||||
|
declare const __USE_QR_READER__: boolean |
||||
|
|
||||
|
export class WebDialogQRScanner implements QRScannerService { |
||||
|
private dialogApp: App | null = null |
||||
|
private dialogElement: HTMLDivElement | null = null |
||||
|
private scanListener: ScanListener | null = null |
||||
|
|
||||
|
async checkPermissions(): Promise<boolean> { |
||||
|
return navigator?.mediaDevices !== undefined |
||||
|
} |
||||
|
|
||||
|
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('Failed to get camera permissions:', error) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async isSupported(): Promise<boolean> { |
||||
|
return Promise.resolve( |
||||
|
__USE_QR_READER__ && navigator?.mediaDevices !== undefined |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
async startScan(): Promise<void> { |
||||
|
if (!(await this.isSupported())) { |
||||
|
throw new Error('QR scanning is not supported in this environment') |
||||
|
} |
||||
|
|
||||
|
this.dialogElement = document.createElement('div') |
||||
|
document.body.appendChild(this.dialogElement) |
||||
|
|
||||
|
this.dialogApp = createApp(QRScannerDialog, { |
||||
|
onScan: (result: string) => { |
||||
|
if (this.scanListener) { |
||||
|
this.scanListener.onScan(result) |
||||
|
} |
||||
|
}, |
||||
|
onClose: () => { |
||||
|
this.stopScan() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
this.dialogApp.mount(this.dialogElement) |
||||
|
} |
||||
|
|
||||
|
async stopScan(): Promise<void> { |
||||
|
if (this.dialogApp && this.dialogElement) { |
||||
|
this.dialogApp.unmount() |
||||
|
this.dialogElement.remove() |
||||
|
this.dialogApp = null |
||||
|
this.dialogElement = null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addListener(listener: ScanListener): void { |
||||
|
this.scanListener = listener |
||||
|
} |
||||
|
|
||||
|
async cleanup(): Promise<void> { |
||||
|
await this.stopScan() |
||||
|
this.scanListener = null |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
import type { QRScannerService } from './types' |
||||
|
import { logger } from '../../utils/logger' |
||||
|
import { WebDialogQRScanner } from './WebDialogScanner' |
||||
|
import { CapacitorQRScanner } from './CapacitorScanner' |
||||
|
|
||||
|
// Import platform-specific flags from Vite config
|
||||
|
declare const __USE_QR_READER__: boolean |
||||
|
declare const __IS_MOBILE__: boolean |
||||
|
|
||||
|
export class QRScannerFactory { |
||||
|
private static instance: QRScannerService | null = null |
||||
|
|
||||
|
static getInstance(): QRScannerService { |
||||
|
if (!this.instance) { |
||||
|
// Use platform-specific flags for more accurate detection
|
||||
|
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { |
||||
|
logger.log('Creating native QR scanner instance') |
||||
|
this.instance = new CapacitorQRScanner() |
||||
|
} else if (__USE_QR_READER__) { |
||||
|
logger.log('Creating web QR scanner instance') |
||||
|
this.instance = new WebDialogQRScanner() |
||||
|
} else { |
||||
|
throw new Error( |
||||
|
'No QR scanner implementation available for this platform' |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
return this.instance |
||||
|
} |
||||
|
|
||||
|
static async cleanup() { |
||||
|
if (this.instance) { |
||||
|
await this.instance.cleanup() |
||||
|
this.instance = null |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
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> |
||||
|
} |
||||
@ -1,19 +1,19 @@ |
|||||
import { PerspectiveCamera } from "three"; |
import { PerspectiveCamera } from 'three' |
||||
|
|
||||
function createCamera() { |
function createCamera() { |
||||
const camera = new PerspectiveCamera( |
const camera = new PerspectiveCamera( |
||||
35, // fov = Field Of View
|
35, // fov = Field Of View
|
||||
1, // aspect ratio (dummy value)
|
1, // aspect ratio (dummy value)
|
||||
0.1, // near clipping plane
|
0.1, // near clipping plane
|
||||
350, // far clipping plane
|
350 // far clipping plane
|
||||
); |
) |
||||
|
|
||||
// move the camera back so we can view the scene
|
// move the camera back so we can view the scene
|
||||
camera.position.set(0, 100, 200); |
camera.position.set(0, 100, 200) |
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
camera.tick = () => {}; |
camera.tick = () => {} |
||||
|
|
||||
return camera; |
return camera |
||||
} |
} |
||||
|
|
||||
export { createCamera }; |
export { createCamera } |
||||
|
|||||
@ -1,14 +1,14 @@ |
|||||
import { DirectionalLight, DirectionalLightHelper } from "three"; |
import { DirectionalLight, DirectionalLightHelper } from 'three' |
||||
|
|
||||
function createLights(color) { |
function createLights(color) { |
||||
const light = new DirectionalLight(color, 4); |
const light = new DirectionalLight(color, 4) |
||||
const lightHelper = new DirectionalLightHelper(light, 0); |
const lightHelper = new DirectionalLightHelper(light, 0) |
||||
light.position.set(60, 100, 30); |
light.position.set(60, 100, 30) |
||||
|
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
light.tick = () => {}; |
light.tick = () => {} |
||||
|
|
||||
return { light, lightHelper }; |
return { light, lightHelper } |
||||
} |
} |
||||
|
|
||||
export { createLights }; |
export { createLights } |
||||
|
|||||
@ -1,29 +1,29 @@ |
|||||
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three"; |
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from 'three' |
||||
|
|
||||
export function createTerrain(props) { |
export function createTerrain(props) { |
||||
const loader = new TextureLoader(); |
const loader = new TextureLoader() |
||||
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg"); |
const height = loader.load('img/textures/leafy-autumn-forest-floor.jpg') |
||||
// w h
|
// w h
|
||||
const geometry = new PlaneGeometry(props.width, props.height, 64, 64); |
const geometry = new PlaneGeometry(props.width, props.height, 64, 64) |
||||
|
|
||||
const material = new MeshLambertMaterial({ |
const material = new MeshLambertMaterial({ |
||||
color: props.color, |
color: props.color, |
||||
flatShading: true, |
flatShading: true, |
||||
map: height, |
map: height |
||||
//displacementMap: height,
|
//displacementMap: height,
|
||||
//displacementScale: 5,
|
//displacementScale: 5,
|
||||
}); |
}) |
||||
|
|
||||
const plane = new Mesh(geometry, material); |
const plane = new Mesh(geometry, material) |
||||
plane.position.set(0, 0, 0); |
plane.position.set(0, 0, 0) |
||||
plane.rotation.x -= Math.PI * 0.5; |
plane.rotation.x -= Math.PI * 0.5 |
||||
|
|
||||
//Storing our original vertices position on a new attribute
|
//Storing our original vertices position on a new attribute
|
||||
plane.geometry.attributes.position.originalPosition = |
plane.geometry.attributes.position.originalPosition = |
||||
plane.geometry.attributes.position.array; |
plane.geometry.attributes.position.array |
||||
|
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
plane.tick = () => {}; |
plane.tick = () => {} |
||||
|
|
||||
return plane; |
return plane |
||||
} |
} |
||||
|
|||||
@ -1,11 +1,11 @@ |
|||||
import { Color, Scene } from "three"; |
import { Color, Scene } from 'three' |
||||
|
|
||||
function createScene(color) { |
function createScene(color) { |
||||
const scene = new Scene(); |
const scene = new Scene() |
||||
|
|
||||
scene.background = new Color(color); |
scene.background = new Color(color) |
||||
//scene.fog = new Fog(color, 60, 90);
|
//scene.fog = new Fog(color, 60, 90);
|
||||
return scene; |
return scene |
||||
} |
} |
||||
|
|
||||
export { createScene }; |
export { createScene } |
||||
|
|||||
@ -1,33 +1,33 @@ |
|||||
import { Clock } from "three"; |
import { Clock } from 'three' |
||||
|
|
||||
const clock = new Clock(); |
const clock = new Clock() |
||||
|
|
||||
class Loop { |
class Loop { |
||||
constructor(camera, scene, renderer) { |
constructor(camera, scene, renderer) { |
||||
this.camera = camera; |
this.camera = camera |
||||
this.scene = scene; |
this.scene = scene |
||||
this.renderer = renderer; |
this.renderer = renderer |
||||
this.updatables = []; |
this.updatables = [] |
||||
} |
} |
||||
|
|
||||
start() { |
start() { |
||||
this.renderer.setAnimationLoop(() => { |
this.renderer.setAnimationLoop(() => { |
||||
this.tick(); |
this.tick() |
||||
// render a frame
|
// render a frame
|
||||
this.renderer.render(this.scene, this.camera); |
this.renderer.render(this.scene, this.camera) |
||||
}); |
}) |
||||
} |
} |
||||
|
|
||||
stop() { |
stop() { |
||||
this.renderer.setAnimationLoop(null); |
this.renderer.setAnimationLoop(null) |
||||
} |
} |
||||
|
|
||||
tick() { |
tick() { |
||||
const delta = clock.getDelta(); |
const delta = clock.getDelta() |
||||
for (const object of this.updatables) { |
for (const object of this.updatables) { |
||||
object.tick(delta); |
object.tick(delta) |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
export { Loop }; |
export { Loop } |
||||
|
|||||
@ -1,33 +1,33 @@ |
|||||
const setSize = (container, camera, renderer) => { |
const setSize = (container, camera, renderer) => { |
||||
// These are great for full-screen, which adjusts to a window.
|
// These are great for full-screen, which adjusts to a window.
|
||||
const height = window.innerHeight; |
const height = window.innerHeight |
||||
const width = window.innerWidth - 50; |
const width = window.innerWidth - 50 |
||||
// These are better for fitting in a container, which stays that size.
|
// These are better for fitting in a container, which stays that size.
|
||||
//const height = container.scrollHeight;
|
//const height = container.scrollHeight;
|
||||
//const width = container.scrollWidth;
|
//const width = container.scrollWidth;
|
||||
|
|
||||
camera.aspect = width / height; |
camera.aspect = width / height |
||||
camera.updateProjectionMatrix(); |
camera.updateProjectionMatrix() |
||||
|
|
||||
renderer.setSize(width, height); |
renderer.setSize(width, height) |
||||
renderer.setPixelRatio(window.devicePixelRatio); |
renderer.setPixelRatio(window.devicePixelRatio) |
||||
}; |
} |
||||
|
|
||||
class Resizer { |
class Resizer { |
||||
constructor(container, camera, renderer) { |
constructor(container, camera, renderer) { |
||||
// set initial size on load
|
// set initial size on load
|
||||
setSize(container, camera, renderer); |
setSize(container, camera, renderer) |
||||
|
|
||||
window.addEventListener("resize", () => { |
window.addEventListener('resize', () => { |
||||
// set the size again if a resize occurs
|
// set the size again if a resize occurs
|
||||
setSize(container, camera, renderer); |
setSize(container, camera, renderer) |
||||
// perform any custom actions
|
// perform any custom actions
|
||||
this.onResize(); |
this.onResize() |
||||
}); |
}) |
||||
} |
} |
||||
|
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onResize() {} |
onResize() {} |
||||
} |
} |
||||
|
|
||||
export { Resizer }; |
export { Resizer } |
||||
|
|||||
@ -1,13 +1,13 @@ |
|||||
import { WebGLRenderer } from "three"; |
import { WebGLRenderer } from 'three' |
||||
|
|
||||
function createRenderer() { |
function createRenderer() { |
||||
const renderer = new WebGLRenderer({ antialias: true }); |
const renderer = new WebGLRenderer({ antialias: true }) |
||||
|
|
||||
// turn on the physically correct lighting model
|
// turn on the physically correct lighting model
|
||||
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
|
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
|
||||
renderer.physicallyCorrectLights = true; |
renderer.physicallyCorrectLights = true |
||||
|
|
||||
return renderer; |
return renderer |
||||
} |
} |
||||
|
|
||||
export { createRenderer }; |
export { createRenderer } |
||||
|
|||||
@ -1,24 +1,24 @@ |
|||||
export interface ContactMethod { |
export interface ContactMethod { |
||||
label: string; |
label: string |
||||
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
type: string // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||
value: string; |
value: string |
||||
} |
} |
||||
|
|
||||
export interface Contact { |
export interface Contact { |
||||
//
|
//
|
||||
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
||||
|
|
||||
did: string; |
did: string |
||||
contactMethods?: Array<ContactMethod>; |
contactMethods?: Array<ContactMethod> |
||||
name?: string; |
name?: string |
||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
nextPubKeyHashB64?: string // base64-encoded SHA256 hash of next public key
|
||||
notes?: string; |
notes?: string |
||||
profileImageUrl?: string; |
profileImageUrl?: string |
||||
publicKeyBase64?: string; |
publicKeyBase64?: string |
||||
seesMe?: boolean; // cached value of the server setting
|
seesMe?: boolean // cached value of the server setting
|
||||
registered?: boolean; // cached value of the server setting
|
registered?: boolean // cached value of the server setting
|
||||
} |
} |
||||
|
|
||||
export const ContactSchema = { |
export const ContactSchema = { |
||||
contacts: "&did, name", // no need to key by other things
|
contacts: '&did, name' // no need to key by other things
|
||||
}; |
} |
||||
|
|||||
@ -1,11 +1,11 @@ |
|||||
export interface Log { |
export interface Log { |
||||
date: string; |
date: string |
||||
message: string; |
message: string |
||||
} |
} |
||||
|
|
||||
export const LogSchema = { |
export const LogSchema = { |
||||
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
||||
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
||||
// See safari-notifications.js logMessage for the associated logic.
|
// See safari-notifications.js logMessage for the associated logic.
|
||||
logs: "date", // definitely don't key by the potentially large message field
|
logs: 'date' // definitely don't key by the potentially large message field
|
||||
}; |
} |
||||
|
|||||
@ -1,12 +1,12 @@ |
|||||
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
|
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
|
||||
|
|
||||
export type Temp = { |
export type Temp = { |
||||
id: string; |
id: string |
||||
blob?: Blob; // deprecated because webkit (Safari) does not support Blob
|
blob?: Blob // deprecated because webkit (Safari) does not support Blob
|
||||
blobB64?: string; // base64-encoded blob
|
blobB64?: string // base64-encoded blob
|
||||
}; |
} |
||||
|
|
||||
/** |
/** |
||||
* Schema for the Temp table in the database. |
* Schema for the Temp table in the database. |
||||
*/ |
*/ |
||||
export const TempSchema = { temp: "id" }; |
export const TempSchema = { temp: 'id' } |
||||
|
|||||
@ -1,78 +1,78 @@ |
|||||
const { contextBridge, ipcRenderer } = require("electron"); |
const { contextBridge, ipcRenderer } = require('electron') |
||||
|
|
||||
const logger = { |
const logger = { |
||||
log: (message, ...args) => { |
log: (message, ...args) => { |
||||
if (process.env.NODE_ENV !== "production") { |
if (process.env.NODE_ENV !== 'production') { |
||||
/* eslint-disable no-console */ |
/* eslint-disable no-console */ |
||||
console.log(message, ...args); |
console.log(message, ...args) |
||||
/* eslint-enable no-console */ |
/* eslint-enable no-console */ |
||||
} |
} |
||||
}, |
}, |
||||
warn: (message, ...args) => { |
warn: (message, ...args) => { |
||||
if (process.env.NODE_ENV !== "production") { |
if (process.env.NODE_ENV !== 'production') { |
||||
/* eslint-disable no-console */ |
/* eslint-disable no-console */ |
||||
console.warn(message, ...args); |
console.warn(message, ...args) |
||||
/* eslint-enable no-console */ |
/* eslint-enable no-console */ |
||||
} |
} |
||||
}, |
}, |
||||
error: (message, ...args) => { |
error: (message, ...args) => { |
||||
/* eslint-disable no-console */ |
/* eslint-disable no-console */ |
||||
console.error(message, ...args); // Errors should always be logged
|
console.error(message, ...args) // Errors should always be logged
|
||||
/* eslint-enable no-console */ |
/* eslint-enable no-console */ |
||||
}, |
} |
||||
}; |
} |
||||
|
|
||||
// Use a more direct path resolution approach
|
// Use a more direct path resolution approach
|
||||
const getPath = (pathType) => { |
const getPath = (pathType) => { |
||||
switch (pathType) { |
switch (pathType) { |
||||
case "userData": |
case 'userData': |
||||
return ( |
return ( |
||||
process.env.APPDATA || |
process.env.APPDATA || |
||||
(process.platform === "darwin" |
(process.platform === 'darwin' |
||||
? `${process.env.HOME}/Library/Application Support` |
? `${process.env.HOME}/Library/Application Support` |
||||
: `${process.env.HOME}/.local/share`) |
: `${process.env.HOME}/.local/share`) |
||||
); |
) |
||||
case "home": |
case 'home': |
||||
return process.env.HOME; |
return process.env.HOME |
||||
case "appPath": |
case 'appPath': |
||||
return process.resourcesPath; |
return process.resourcesPath |
||||
default: |
default: |
||||
return ""; |
return '' |
||||
} |
} |
||||
}; |
} |
||||
|
|
||||
logger.log("Preload script starting..."); |
logger.log('Preload script starting...') |
||||
|
|
||||
try { |
try { |
||||
contextBridge.exposeInMainWorld("electronAPI", { |
contextBridge.exposeInMainWorld('electronAPI', { |
||||
// Path utilities
|
// Path utilities
|
||||
getPath, |
getPath, |
||||
|
|
||||
// IPC functions
|
// IPC functions
|
||||
send: (channel, data) => { |
send: (channel, data) => { |
||||
const validChannels = ["toMain"]; |
const validChannels = ['toMain'] |
||||
if (validChannels.includes(channel)) { |
if (validChannels.includes(channel)) { |
||||
ipcRenderer.send(channel, data); |
ipcRenderer.send(channel, data) |
||||
} |
} |
||||
}, |
}, |
||||
receive: (channel, func) => { |
receive: (channel, func) => { |
||||
const validChannels = ["fromMain"]; |
const validChannels = ['fromMain'] |
||||
if (validChannels.includes(channel)) { |
if (validChannels.includes(channel)) { |
||||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
ipcRenderer.on(channel, (event, ...args) => func(...args)) |
||||
} |
} |
||||
}, |
}, |
||||
// Environment info
|
// Environment info
|
||||
env: { |
env: { |
||||
isElectron: true, |
isElectron: true, |
||||
isDev: process.env.NODE_ENV === "development", |
isDev: process.env.NODE_ENV === 'development' |
||||
}, |
}, |
||||
// Path utilities
|
// Path utilities
|
||||
getBasePath: () => { |
getBasePath: () => { |
||||
return process.env.NODE_ENV === "development" ? "/" : "./"; |
return process.env.NODE_ENV === 'development' ? '/' : './' |
||||
}, |
} |
||||
}); |
}) |
||||
|
|
||||
logger.log("Preload script completed successfully"); |
logger.log('Preload script completed successfully') |
||||
} catch (error) { |
} catch (error) { |
||||
logger.error("Error in preload script:", error); |
logger.error('Error in preload script:', error) |
||||
} |
} |
||||
|
|||||
@ -1,58 +1,58 @@ |
|||||
import { AxiosResponse } from "axios"; |
import { AxiosResponse } from 'axios' |
||||
import { GiverReceiverInputInfo } from "../libs/util"; |
import { GiverReceiverInputInfo } from '../libs/util' |
||||
import { ErrorResult, ResultWithType } from "./common"; |
import { ErrorResult, ResultWithType } from './common' |
||||
|
|
||||
export interface GiverOutputInfo { |
export interface GiverOutputInfo { |
||||
action: string; |
action: string |
||||
giver?: GiverReceiverInputInfo; |
giver?: GiverReceiverInputInfo |
||||
description?: string; |
description?: string |
||||
amount?: number; |
amount?: number |
||||
unitCode?: string; |
unitCode?: string |
||||
} |
} |
||||
|
|
||||
export interface ClaimResult { |
export interface ClaimResult { |
||||
success: { claimId: string; handleId: string }; |
success: { claimId: string; handleId: string } |
||||
error: { code: string; message: string }; |
error: { code: string; message: string } |
||||
} |
} |
||||
|
|
||||
export interface VerifiableCredential { |
export interface VerifiableCredential { |
||||
exp?: number; |
exp?: number |
||||
iat: number; |
iat: number |
||||
iss: string; |
iss: string |
||||
vc: { |
vc: { |
||||
"@context": string[]; |
'@context': string[] |
||||
type: string[]; |
type: string[] |
||||
credentialSubject: VerifiableCredentialSubject; |
credentialSubject: VerifiableCredentialSubject |
||||
}; |
} |
||||
} |
} |
||||
|
|
||||
export interface VerifiableCredentialSubject { |
export interface VerifiableCredentialSubject { |
||||
"@context": string; |
'@context': string |
||||
"@type": string; |
'@type': string |
||||
[key: string]: unknown; |
[key: string]: unknown |
||||
} |
} |
||||
|
|
||||
export interface WorldProperties { |
export interface WorldProperties { |
||||
startTime?: string; |
startTime?: string |
||||
endTime?: string; |
endTime?: string |
||||
} |
} |
||||
|
|
||||
export interface ProviderInfo { |
export interface ProviderInfo { |
||||
/** |
/** |
||||
* Could be a DID or a handleId that identifies the provider |
* Could be a DID or a handleId that identifies the provider |
||||
*/ |
*/ |
||||
identifier: string; |
identifier: string |
||||
/** |
/** |
||||
* Indicates if the provider link has been confirmed |
* Indicates if the provider link has been confirmed |
||||
*/ |
*/ |
||||
linkConfirmed: boolean; |
linkConfirmed: boolean |
||||
} |
} |
||||
|
|
||||
// Type for createAndSubmitClaim result
|
// Type for createAndSubmitClaim result
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult; |
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult |
||||
|
|
||||
// Update SuccessResult to use ClaimResult
|
// Update SuccessResult to use ClaimResult
|
||||
export interface SuccessResult extends ResultWithType { |
export interface SuccessResult extends ResultWithType { |
||||
type: "success"; |
type: 'success' |
||||
response: AxiosResponse<ClaimResult>; |
response: AxiosResponse<ClaimResult> |
||||
} |
} |
||||
|
|||||
@ -1,68 +1,68 @@ |
|||||
import { GenericVerifiableCredential } from "./common"; |
import { GenericVerifiableCredential } from './common' |
||||
|
|
||||
export interface AgreeVerifiableCredential { |
export interface AgreeVerifiableCredential { |
||||
"@context": string; |
'@context': string |
||||
"@type": string; |
'@type': string |
||||
object: Record<string, unknown>; |
object: Record<string, unknown> |
||||
} |
} |
||||
|
|
||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential { |
export interface GiveVerifiableCredential extends GenericVerifiableCredential { |
||||
"@context"?: string; |
'@context'?: string |
||||
"@type": "GiveAction"; |
'@type': 'GiveAction' |
||||
agent?: { identifier: string }; |
agent?: { identifier: string } |
||||
description?: string; |
description?: string |
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[]; |
fulfills?: { '@type': string; identifier?: string; lastClaimId?: string }[] |
||||
identifier?: string; |
identifier?: string |
||||
image?: string; |
image?: string |
||||
object?: { amountOfThisGood: number; unitCode: string }; |
object?: { amountOfThisGood: number; unitCode: string } |
||||
provider?: GenericVerifiableCredential; |
provider?: GenericVerifiableCredential |
||||
recipient?: { identifier: string }; |
recipient?: { identifier: string } |
||||
} |
} |
||||
|
|
||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential { |
export interface OfferVerifiableCredential extends GenericVerifiableCredential { |
||||
"@context"?: string; |
'@context'?: string |
||||
"@type": "Offer"; |
'@type': 'Offer' |
||||
description?: string; |
description?: string |
||||
includesObject?: { amountOfThisGood: number; unitCode: string }; |
includesObject?: { amountOfThisGood: number; unitCode: string } |
||||
itemOffered?: { |
itemOffered?: { |
||||
description?: string; |
description?: string |
||||
isPartOf?: { |
isPartOf?: { |
||||
identifier?: string; |
identifier?: string |
||||
lastClaimId?: string; |
lastClaimId?: string |
||||
"@type"?: string; |
'@type'?: string |
||||
name?: string; |
name?: string |
||||
}; |
} |
||||
}; |
} |
||||
offeredBy?: { identifier: string }; |
offeredBy?: { identifier: string } |
||||
recipient?: { identifier: string }; |
recipient?: { identifier: string } |
||||
validThrough?: string; |
validThrough?: string |
||||
} |
} |
||||
|
|
||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential { |
export interface PlanVerifiableCredential extends GenericVerifiableCredential { |
||||
"@context": "https://schema.org"; |
'@context': 'https://schema.org' |
||||
"@type": "PlanAction"; |
'@type': 'PlanAction' |
||||
name: string; |
name: string |
||||
agent?: { identifier: string }; |
agent?: { identifier: string } |
||||
description?: string; |
description?: string |
||||
identifier?: string; |
identifier?: string |
||||
lastClaimId?: string; |
lastClaimId?: string |
||||
location?: { |
location?: { |
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number }; |
geo: { '@type': 'GeoCoordinates'; latitude: number; longitude: number } |
||||
}; |
} |
||||
} |
} |
||||
|
|
||||
// AKA Registration & RegisterAction
|
// AKA Registration & RegisterAction
|
||||
export interface RegisterVerifiableCredential { |
export interface RegisterVerifiableCredential { |
||||
"@context": string; |
'@context': string |
||||
"@type": "RegisterAction"; |
'@type': 'RegisterAction' |
||||
agent: { identifier: string }; |
agent: { identifier: string } |
||||
identifier?: string; |
identifier?: string |
||||
object: string; |
object: string |
||||
participant?: { identifier: string }; |
participant?: { identifier: string } |
||||
} |
} |
||||
|
|||||
@ -1,36 +1,36 @@ |
|||||
// similar to VerifiableCredentialSubject... maybe rename this
|
// similar to VerifiableCredentialSubject... maybe rename this
|
||||
export interface GenericVerifiableCredential { |
export interface GenericVerifiableCredential { |
||||
"@context"?: string; |
'@context'?: string |
||||
"@type": string; |
'@type': string |
||||
[key: string]: unknown; |
[key: string]: unknown |
||||
} |
} |
||||
|
|
||||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> { |
export interface GenericCredWrapper<T extends GenericVerifiableCredential> { |
||||
claim: T; |
claim: T |
||||
claimType?: string; |
claimType?: string |
||||
handleId: string; |
handleId: string |
||||
id: string; |
id: string |
||||
issuedAt: string; |
issuedAt: string |
||||
issuer: string; |
issuer: string |
||||
publicUrls?: Record<string, string>; |
publicUrls?: Record<string, string> |
||||
} |
} |
||||
|
|
||||
export interface ResultWithType { |
export interface ResultWithType { |
||||
type: string; |
type: string |
||||
} |
} |
||||
|
|
||||
export interface ErrorResponse { |
export interface ErrorResponse { |
||||
error?: { |
error?: { |
||||
message?: string; |
message?: string |
||||
}; |
} |
||||
} |
} |
||||
|
|
||||
export interface InternalError { |
export interface InternalError { |
||||
error: string; |
error: string |
||||
userMessage?: string; |
userMessage?: string |
||||
} |
} |
||||
|
|
||||
export interface ErrorResult extends ResultWithType { |
export interface ErrorResult extends ResultWithType { |
||||
type: "error"; |
type: 'error' |
||||
error: InternalError; |
error: InternalError |
||||
} |
} |
||||
|
|||||
@ -1,7 +1,7 @@ |
|||||
export * from "./claims"; |
export * from './claims' |
||||
export * from "./claims-result"; |
export * from './claims-result' |
||||
export * from "./common"; |
export * from './common' |
||||
export * from "./limits"; |
export * from './limits' |
||||
export * from "./records"; |
export * from './records' |
||||
export * from "./user"; |
export * from './user' |
||||
export * from "./deepLinks"; |
export * from './deepLinks' |
||||
|
|||||
@ -1,14 +1,14 @@ |
|||||
export interface EndorserRateLimits { |
export interface EndorserRateLimits { |
||||
doneClaimsThisWeek: string; |
doneClaimsThisWeek: string |
||||
doneRegistrationsThisMonth: string; |
doneRegistrationsThisMonth: string |
||||
maxClaimsPerWeek: string; |
maxClaimsPerWeek: string |
||||
maxRegistrationsPerMonth: string; |
maxRegistrationsPerMonth: string |
||||
nextMonthBeginDateTime: string; |
nextMonthBeginDateTime: string |
||||
nextWeekBeginDateTime: string; |
nextWeekBeginDateTime: string |
||||
} |
} |
||||
|
|
||||
export interface ImageRateLimits { |
export interface ImageRateLimits { |
||||
doneImagesThisWeek: string; |
doneImagesThisWeek: string |
||||
maxImagesPerWeek: string; |
maxImagesPerWeek: string |
||||
nextWeekBeginDateTime: string; |
nextWeekBeginDateTime: string |
||||
} |
} |
||||
|
|||||
@ -1,8 +1,8 @@ |
|||||
export interface UserInfo { |
export interface UserInfo { |
||||
did: string; |
did: string |
||||
name: string; |
name: string |
||||
publicEncKey: string; |
publicEncKey: string |
||||
registered: boolean; |
registered: boolean |
||||
profileImageUrl?: string; |
profileImageUrl?: string |
||||
nextPublicEncKeyHash?: string; |
nextPublicEncKeyHash?: string |
||||
} |
} |
||||
|
|||||
@ -1,11 +1,11 @@ |
|||||
export function urlBase64ToUint8Array(base64String: string): Uint8Array { |
export function urlBase64ToUint8Array(base64String: string): Uint8Array { |
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); |
const padding = '='.repeat((4 - (base64String.length % 4)) % 4) |
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); |
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') |
||||
const rawData = window.atob(base64); |
const rawData = window.atob(base64) |
||||
const outputArray = new Uint8Array(rawData.length); |
const outputArray = new Uint8Array(rawData.length) |
||||
|
|
||||
for (let i = 0; i < rawData.length; ++i) { |
for (let i = 0; i < rawData.length; ++i) { |
||||
outputArray[i] = rawData.charCodeAt(i); |
outputArray[i] = rawData.charCodeAt(i) |
||||
} |
} |
||||
return outputArray; |
return outputArray |
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
@ -1,9 +1,9 @@ |
|||||
export interface UserProfile { |
export interface UserProfile { |
||||
description: string; |
description: string |
||||
locLat?: number; |
locLat?: number |
||||
locLon?: number; |
locLon?: number |
||||
locLat2?: number; |
locLat2?: number |
||||
locLon2?: number; |
locLon2?: number |
||||
issuerDid: string; |
issuerDid: string |
||||
rowId?: string; // set on profile retrieved from server
|
rowId?: string // set on profile retrieved from server
|
||||
} |
} |
||||
|
|||||
@ -1,7 +1,7 @@ |
|||||
// see also ../constants/app.ts and
|
// see also ../constants/app.ts and
|
||||
|
|
||||
function didProviderName(netName: string) { |
function didProviderName(netName: string) { |
||||
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName); |
return 'did:ethr' + (netName === 'mainnet' ? '' : ':' + netName) |
||||
} |
} |
||||
|
|
||||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet"); |
export const DEFAULT_DID_PROVIDER_NAME = didProviderName('mainnet') |
||||
|
|||||
@ -1,61 +1,61 @@ |
|||||
import { createPinia } from "pinia"; |
import { createPinia } from 'pinia' |
||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue"; |
import { App as VueApp, ComponentPublicInstance, createApp } from 'vue' |
||||
import App from "./App.vue"; |
import App from './App.vue' |
||||
import router from "./router"; |
import router from './router' |
||||
import axios from "axios"; |
import axios from 'axios' |
||||
import VueAxios from "vue-axios"; |
import VueAxios from 'vue-axios' |
||||
import Notifications from "notiwind"; |
import Notifications from 'notiwind' |
||||
import "./assets/styles/tailwind.css"; |
import './assets/styles/tailwind.css' |
||||
import { FontAwesomeIcon } from "./lib/fontawesome"; |
import { FontAwesomeIcon } from './lib/fontawesome' |
||||
import Camera from "simple-vue-camera"; |
import Camera from 'simple-vue-camera' |
||||
import { logger } from "./utils/logger"; |
import { logger } from './utils/logger' |
||||
|
|
||||
// Global Error Handler
|
// Global Error Handler
|
||||
function setupGlobalErrorHandler(app: VueApp) { |
function setupGlobalErrorHandler(app: VueApp) { |
||||
logger.log("[App Init] Setting up global error handler"); |
logger.log('[App Init] Setting up global error handler') |
||||
app.config.errorHandler = ( |
app.config.errorHandler = ( |
||||
err: unknown, |
err: unknown, |
||||
instance: ComponentPublicInstance | null, |
instance: ComponentPublicInstance | null, |
||||
info: string, |
info: string |
||||
) => { |
) => { |
||||
logger.error("[App Error] Global Error Handler:", { |
logger.error('[App Error] Global Error Handler:', { |
||||
error: err, |
error: err, |
||||
info, |
info, |
||||
component: instance?.$options.name || "unknown", |
component: instance?.$options.name || 'unknown' |
||||
}); |
}) |
||||
alert( |
alert( |
||||
(err instanceof Error ? err.message : "Something bad happened") + |
(err instanceof Error ? err.message : 'Something bad happened') + |
||||
" - Try reloading or restarting the app.", |
' - Try reloading or restarting the app.' |
||||
); |
) |
||||
}; |
} |
||||
} |
} |
||||
|
|
||||
// Function to initialize the app
|
// Function to initialize the app
|
||||
export function initializeApp() { |
export function initializeApp() { |
||||
logger.log("[App Init] Starting app initialization"); |
logger.log('[App Init] Starting app initialization') |
||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM); |
logger.log('[App Init] Platform:', process.env.VITE_PLATFORM) |
||||
|
|
||||
const app = createApp(App); |
const app = createApp(App) |
||||
logger.log("[App Init] Vue app created"); |
logger.log('[App Init] Vue app created') |
||||
|
|
||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera); |
app.component('FontAwesome', FontAwesomeIcon).component('camera', Camera) |
||||
logger.log("[App Init] Components registered"); |
logger.log('[App Init] Components registered') |
||||
|
|
||||
const pinia = createPinia(); |
const pinia = createPinia() |
||||
app.use(pinia); |
app.use(pinia) |
||||
logger.log("[App Init] Pinia store initialized"); |
logger.log('[App Init] Pinia store initialized') |
||||
|
|
||||
app.use(VueAxios, axios); |
app.use(VueAxios, axios) |
||||
logger.log("[App Init] Axios initialized"); |
logger.log('[App Init] Axios initialized') |
||||
|
|
||||
app.use(router); |
app.use(router) |
||||
logger.log("[App Init] Router initialized"); |
logger.log('[App Init] Router initialized') |
||||
|
|
||||
app.use(Notifications); |
app.use(Notifications) |
||||
logger.log("[App Init] Notifications initialized"); |
logger.log('[App Init] Notifications initialized') |
||||
|
|
||||
setupGlobalErrorHandler(app); |
setupGlobalErrorHandler(app) |
||||
logger.log("[App Init] App initialization complete"); |
logger.log('[App Init] App initialization complete') |
||||
|
|
||||
return app; |
return app |
||||
} |
} |
||||
|
|||||
@ -1,4 +1,4 @@ |
|||||
import { initializeApp } from "./main.common"; |
import { initializeApp } from './main.common' |
||||
|
|
||||
const app = initializeApp(); |
const app = initializeApp() |
||||
app.mount("#app"); |
app.mount('#app') |
||||
|
|||||
@ -1,4 +1,4 @@ |
|||||
import { initializeApp } from "./main.common"; |
import { initializeApp } from './main.common' |
||||
|
|
||||
const app = initializeApp(); |
const app = initializeApp() |
||||
app.mount("#app"); |
app.mount('#app') |
||||
|
|||||
@ -1,5 +1,5 @@ |
|||||
import { initializeApp } from "./main.common"; |
import { initializeApp } from './main.common' |
||||
import "./registerServiceWorker"; // Web PWA support
|
import './registerServiceWorker' // Web PWA support
|
||||
|
|
||||
const app = initializeApp(); |
const app = initializeApp() |
||||
app.mount("#app"); |
app.mount('#app') |
||||
|
|||||
@ -1,39 +1,39 @@ |
|||||
/* eslint-disable no-console */ |
/* eslint-disable no-console */ |
||||
|
|
||||
import { register } from "register-service-worker"; |
import { register } from 'register-service-worker' |
||||
|
|
||||
// Only register service worker if explicitly enabled and in production
|
// Only register service worker if explicitly enabled and in production
|
||||
if ( |
if ( |
||||
process.env.VITE_PWA_ENABLED === "true" && |
process.env.VITE_PWA_ENABLED === 'true' && |
||||
process.env.NODE_ENV === "production" |
process.env.NODE_ENV === 'production' |
||||
) { |
) { |
||||
register(`${process.env.BASE_URL}sw.js`, { |
register(`${process.env.BASE_URL}sw.js`, { |
||||
ready() { |
ready() { |
||||
console.log("Service worker is active."); |
console.log('Service worker is active.') |
||||
}, |
}, |
||||
registered() { |
registered() { |
||||
console.log("Service worker has been registered."); |
console.log('Service worker has been registered.') |
||||
}, |
}, |
||||
cached() { |
cached() { |
||||
console.log("Content has been cached for offline use."); |
console.log('Content has been cached for offline use.') |
||||
}, |
}, |
||||
updatefound() { |
updatefound() { |
||||
console.log("New content is downloading."); |
console.log('New content is downloading.') |
||||
}, |
}, |
||||
updated() { |
updated() { |
||||
console.log("New content is available; please refresh."); |
console.log('New content is available; please refresh.') |
||||
}, |
}, |
||||
offline() { |
offline() { |
||||
console.log( |
console.log( |
||||
"No internet connection found. App is running in offline mode.", |
'No internet connection found. App is running in offline mode.' |
||||
); |
) |
||||
}, |
}, |
||||
error(error) { |
error(error) { |
||||
console.error("Error during service worker registration:", error); |
console.error('Error during service worker registration:', error) |
||||
}, |
} |
||||
}); |
}) |
||||
} else { |
} else { |
||||
console.log( |
console.log( |
||||
"Service worker registration skipped - not enabled or not in production", |
'Service worker registration skipped - not enabled or not in production' |
||||
); |
) |
||||
} |
} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue