Compare commits
397 Commits
fix-servic
...
onboarding
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e6e959b19 | ||
| 1b283a0045 | |||
| afd407e178 | |||
|
|
59b13823c8 | ||
| 3baa6633a6 | |||
| bda98eb632 | |||
| eea1cb995a | |||
| 276e0a741b | |||
| e46d6133fb | |||
| 94994a7251 | |||
| 838723c26b | |||
| bb6eb92ba1 | |||
| a997d4cb92 | |||
| 73733345ff | |||
| 5aa693de63 | |||
| 6f2272eea7 | |||
| 9b69c0b22c | |||
| ab2270d8b2 | |||
| 0cf5cf266d | |||
|
|
4d01f64fe7 | ||
|
|
d1f61e3530 | ||
| 4162208b7f | |||
| 731605e244 | |||
| 5dd2bf4c6e | |||
| 75a4a1d901 | |||
| 8e605d04d7 | |||
| da0b244bae | |||
| 6136cafd11 | |||
| a248e9a5a3 | |||
| 452ae555bb | |||
| 3df5e19d9d | |||
| 8eff407a9c | |||
| e759e4785b | |||
|
|
9d054074e4 | ||
|
|
30de30e709 | ||
|
|
6cbd32af94 | ||
|
|
30c8b73041 | ||
|
|
2f9ab14c88 | ||
|
|
8a7f142cb7 | ||
|
|
f375a4e11a | ||
| 3118f71320 | |||
| d12f23aa81 | |||
| e9a8a3c1e7 | |||
| 1e0efe6011 | |||
| 16557f1e4b | |||
| c4a54967bc | |||
| 20ade415dc | |||
| 6689520270 | |||
| 3fd6c2b80d | |||
|
|
40a2491d68 | ||
|
|
25c1d6ef4e | ||
| a5c5c2b9dd | |||
| cf33a39fbc | |||
| 8629cefa13 | |||
| 5e851e442f | |||
| 4a43bc9c6c | |||
| 60de8cee62 | |||
|
|
bb2a4ab76e | ||
|
|
048dded278 | ||
| e240c2940a | |||
| 54dca9e745 | |||
| 9f0fed0a60 | |||
| 0d152adbf2 | |||
| cead308800 | |||
| 676a301331 | |||
| d6db81cc36 | |||
|
|
f2ddcd2541 | ||
| fb81f7b96e | |||
| a23416ead1 | |||
| 530c7c1a13 | |||
| f255ea389b | |||
| 0d343b9877 | |||
| df06100c32 | |||
|
|
ac5ddfc6f2 | ||
|
|
89b3f30466 | ||
|
|
3cb5cc096b | ||
|
|
5df560154f | ||
|
|
c1aa522e6c | ||
| a082469a01 | |||
|
|
3544d7278d | ||
|
|
d3110506ea | ||
| 8609f8458d | |||
| 8f5c34bc5f | |||
| b0d61b95ea | |||
| af7bd236a3 | |||
| d719338bcc | |||
| 6ddf2d1012 | |||
|
|
1b2d4b623a | ||
|
|
16d5c917d2 | ||
| 5976a4995e | |||
| dcd0cc4c20 | |||
| b3ca6c9d91 | |||
| e9d800f601 | |||
| b939a5e592 | |||
| aa62037fae | |||
| 722020ea86 | |||
| 96aa3f4a54 | |||
| c0c5f9842b | |||
| be27ca1855 | |||
| 92e4570672 | |||
| 820ae727ed | |||
| dbeb1c6b4b | |||
| 573e4b206a | |||
| abc05d426e | |||
| 2ea7479d75 | |||
| 9ac9713172 | |||
| 41dad3254d | |||
| 485eac59a0 | |||
|
|
73fc32b75d | ||
|
|
3d8e40e92b | ||
| 38e67f3533 | |||
| 7f63ee7c80 | |||
| 6a47f0d3e7 | |||
| fc50a9d4c6 | |||
|
|
45f43ff363 | ||
|
|
7b1d4c4849 | ||
|
|
c1f2c3951a | ||
| 9d4f726c31 | |||
| 1d7f626645 | |||
| c5228ba7ec | |||
| 6e1fcd8dee | |||
| 5bb563d694 | |||
| a3951c9d66 | |||
| aa177a9b8c | |||
| 03cb4720b8 | |||
|
|
0e65431f43 | ||
| 297c5a2dbb | |||
|
|
92b9c9334c | ||
|
|
706182ca0c | ||
|
|
68e0fc4976 | ||
| 504056eb90 | |||
| 5a1007c49c | |||
|
|
cbc14e21ec | ||
| ef3bfcdbd2 | |||
| ec1f27bab1 | |||
| 01c33069c4 | |||
| c637d39dc9 | |||
| 3e90bafbd1 | |||
|
|
d2c3e5db05 | ||
|
|
e824fcce2e | ||
|
|
f2c49872a6 | ||
|
|
229d9184b2 | ||
|
|
29908b77e3 | ||
|
|
3e02b3924a | ||
|
|
16cad04e5c | ||
|
|
e4f859a116 | ||
|
|
7f17a3d9c7 | ||
|
|
2d4d9691ca | ||
|
|
63575b36ed | ||
|
|
2eb46367bc | ||
|
|
cea0456148 | ||
|
|
6f5db13a49 | ||
|
|
068662625d | ||
|
|
23627835f9 | ||
|
|
f1ba6f9231 | ||
|
|
137fce3e30 | ||
|
|
7166dadbc0 | ||
|
|
bc274bdf7f | ||
|
|
082f8c0126 | ||
|
|
fd09c7e426 | ||
|
|
be40643379 | ||
|
|
835a270e65 | ||
|
|
8b03789941 | ||
|
|
b4a6b99301 | ||
|
|
13682a1930 | ||
|
|
669a66c24c | ||
|
|
13505b539e | ||
|
|
07ac340733 | ||
|
|
ba2b2fc543 | ||
| 21184e7625 | |||
| 8d1511e38f | |||
|
|
b18112b869 | ||
|
|
a228a9b1c0 | ||
|
|
1560ff0829 | ||
|
|
e839997f91 | ||
|
|
d8d054a0e1 | ||
|
|
efc720e47f | ||
|
|
0a85bea533 | ||
| 7de4125eb7 | |||
|
|
81d4f0c762 | ||
| 4c1b4fe651 | |||
|
|
e63541ef53 | ||
| 0bfc18c385 | |||
|
|
35f5df6b6b | ||
|
|
0f1ac2b230 | ||
| 3c0bdeaed3 | |||
| 11f2527b04 | |||
| 5d8175aeeb | |||
| b6b95cb0d0 | |||
| 655c5188a4 | |||
| 8b7451330f | |||
| b8fbc3f7a6 | |||
| 92dadba1cb | |||
| 3a6f585de0 | |||
|
|
47501ae917 | ||
|
|
28634839ec | ||
| 2647c5a77d | |||
|
|
682fceb1c6 | ||
|
|
e0013008b4 | ||
| 0674d98670 | |||
|
|
ee441d1aea | ||
|
|
75f6e99200 | ||
|
|
52c9e57ef4 | ||
| 603823d808 | |||
| 5f24f4975d | |||
| 5057d7d07f | |||
| 946e88d903 | |||
|
|
cbfb1ebf57 | ||
| a38934e38d | |||
| a3bdcfd168 | |||
| 83771caee1 | |||
| da35b225cd | |||
| 8c3920e108 | |||
| 54f269054f | |||
|
|
574520d9b3 | ||
| 6556eb55a3 | |||
|
|
28e848e386 | ||
|
|
55f56174a5 | ||
| 634e2bb2fb | |||
|
|
30fb3aee8e | ||
| e254837951 | |||
| 8417cad2f3 | |||
| 0f56b659c1 | |||
| a8bfcb720a | |||
| c23e30c431 | |||
| 1129a13e20 | |||
| 5b6c59c232 | |||
| 295a2d9f63 | |||
|
|
6e14ccdbbc | ||
|
|
d636b21744 | ||
| 37b7c4ed36 | |||
|
|
f7728aadf0 | ||
|
|
ce34257ba1 | ||
|
|
190c972f57 | ||
|
|
831df4b253 | ||
|
|
55176ed5db | ||
|
|
b491262bef | ||
|
|
a1c18458e7 | ||
|
|
995af4e576 | ||
|
|
8ac728d488 | ||
|
|
913f11b66c | ||
|
|
79882715d8 | ||
| 22978a1eda | |||
| 79b2218129 | |||
| 52685702c1 | |||
| d555bc3e9c | |||
|
|
141415977e | ||
|
|
981ccbf269 | ||
|
|
b74ec8ecbb | ||
|
|
7b3b1c930e | ||
|
|
85aa2981ad | ||
|
|
a86e577127 | ||
|
|
788d162b1c | ||
|
|
616a69b7fd | ||
|
|
efab9b968c | ||
|
|
70174aea93 | ||
|
|
7f12595c91 | ||
|
|
8f0d09e480 | ||
|
|
cfc0730e75 | ||
| bfbb9a933d | |||
| 674bbfa00c | |||
| 80b754246e | |||
| 5fcf6a1f90 | |||
| 9da12e76fd | |||
|
|
04193f61c7 | ||
|
|
0ca4916a05 | ||
| 925ce830b4 | |||
|
|
d14635c44d | ||
|
|
eb5c9565a6 | ||
|
|
ea108b754e | ||
|
|
e4155e1a20 | ||
|
|
7e9682ce67 | ||
| c7f1148fe4 | |||
| ae9f1ee09f | |||
| 4d0463f7f7 | |||
| 1b7c96ed9b | |||
| 41365fab8f | |||
| 748c4c7a50 | |||
| 35bb9d2207 | |||
|
|
fd914aa46c | ||
|
|
ba1453104f | ||
|
|
3c7f13d604 | ||
|
|
8e8eef2ab5 | ||
|
|
ea17ef930c | ||
|
|
5242a24110 | ||
|
|
93e860e0ac | ||
|
|
f874973bfa | ||
|
|
74b9caa94f | ||
|
|
fdd1ff80ad | ||
|
|
5d195d06ba | ||
|
|
79707d2811 | ||
|
|
9b73e05bdb | ||
|
|
1b7c5decd3 | ||
|
|
8c8fb6fe7d | ||
|
|
29983f11a9 | ||
|
|
5c559606df | ||
|
|
37166fc141 | ||
|
|
01ef7c1fa9 | ||
|
|
2bb71653ac | ||
|
|
7baae7ea7a | ||
|
|
cb1d979431 | ||
|
|
b999a04595 | ||
|
|
0f9826a39d | ||
|
|
8cc17bd09d | ||
|
|
9dc9878472 | ||
|
|
22283e32f2 | ||
|
|
99863ec186 | ||
|
|
8d2dffb012 | ||
|
|
538cbef701 | ||
|
|
7b7940189e | ||
|
|
35b038036a | ||
|
|
b9cafbe269 | ||
|
|
559f52e6d6 | ||
|
|
eb44b624d6 | ||
|
|
6fdbc7f588 | ||
|
|
7e8caae69a | ||
|
|
7b29232b2c | ||
|
|
e7cb5ffd33 | ||
|
|
272f2a91a6 | ||
|
|
f750ea5d10 | ||
|
|
78116329d4 | ||
|
|
2753e142cf | ||
| 9a840ab74a | |||
|
|
c6c49260ef | ||
|
|
87438e7b6b | ||
|
|
3ce2ea9b4e | ||
|
|
8e6ba68560 | ||
|
|
ca9ca5fca7 | ||
|
|
4abb188da3 | ||
|
|
30e448faf8 | ||
|
|
a8812714a3 | ||
|
|
2855d4b8d5 | ||
|
|
b85e6d2958 | ||
|
|
7d260365be | ||
|
|
72de271f6c | ||
|
|
2055097cf2 | ||
|
|
6b38b1a347 | ||
| ca455e9593 | |||
| 5ada70b05e | |||
|
|
4f9b146a66 | ||
|
|
2b638ce2a7 | ||
|
|
0b528af2a6 | ||
|
|
008211bc21 | ||
|
|
6955a36458 | ||
|
|
ba079ea983 | ||
|
|
d7b3c5ec9d | ||
|
|
d83a25f47e | ||
|
|
fb40dc0ff7 | ||
| 5cc42be58a | |||
| 3d1a2eeb8d | |||
|
|
d03fa55001 | ||
|
|
c8eff4d39e | ||
|
|
b8a7771edf | ||
|
|
5d845fb112 | ||
|
|
660f2170de | ||
|
|
94bd649003 | ||
|
|
b2d628cfeb | ||
|
|
00e52f8dca | ||
| 7b0ee2e44e | |||
| ac018997e8 | |||
| 6f449e9c1f | |||
| 543599a6a1 | |||
|
|
073ce24f43 | ||
|
|
2c84bb50b3 | ||
|
|
abf18835f6 | ||
|
|
f72562804d | ||
|
|
bdc5ffafc1 | ||
| 634395ff38 | |||
| da1f08ebaa | |||
| 4ee3ce0061 | |||
| 654c67af72 | |||
| b244f609b3 | |||
| 9c84302c2e | |||
| ca37c30180 | |||
| 130139e2af | |||
| 9802deb17c | |||
| 76c983ea3e | |||
| 114ef440b8 | |||
| b58d510f24 | |||
|
|
da6a5ee83e | ||
|
|
7af39d322f | ||
|
|
bab802160f | ||
|
|
01d7bc9e27 | ||
|
|
fa20360d87 | ||
|
|
770c0fa77c | ||
|
|
0709d0c726 | ||
|
|
d943983bf8 | ||
| be9465e9f8 | |||
| 5606f2a18a | |||
|
|
06e9950e53 | ||
|
|
5dbd66e51b | ||
|
|
312b4aaaa3 | ||
|
|
3a6a24d923 | ||
|
|
d7afb80a07 | ||
|
|
751df09fe5 | ||
|
|
8858495f73 | ||
|
|
ecb088bee2 |
153
.cursor/rules/absurd-sql.mdc
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Absurd SQL - Cursor Development Guide
|
||||
|
||||
## Project Overview
|
||||
Absurd SQL is a backend implementation for sql.js that enables persistent SQLite databases in the browser by using IndexedDB as a block storage system. This guide provides rules and best practices for developing with this project in Cursor.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
absurd-sql/
|
||||
├── src/ # Source code
|
||||
├── dist/ # Built files
|
||||
├── package.json # Dependencies and scripts
|
||||
├── rollup.config.js # Build configuration
|
||||
└── jest.config.js # Test configuration
|
||||
```
|
||||
|
||||
## Development Rules
|
||||
|
||||
### 1. Worker Thread Requirements
|
||||
- All SQL operations MUST be performed in a worker thread
|
||||
- Main thread should only handle worker initialization and communication
|
||||
- Never block the main thread with database operations
|
||||
|
||||
### 2. Code Organization
|
||||
- Keep worker code in separate files (e.g., `*.worker.js`)
|
||||
- Use ES modules for imports/exports
|
||||
- Follow the project's existing module structure
|
||||
|
||||
### 3. Required Headers
|
||||
When developing locally or deploying, ensure these headers are set:
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### 4. Browser Compatibility
|
||||
- Primary target: Modern browsers with SharedArrayBuffer support
|
||||
- Fallback mode: Safari (with limitations)
|
||||
- Always test in both modes
|
||||
|
||||
### 5. Database Configuration
|
||||
Recommended database settings:
|
||||
```sql
|
||||
PRAGMA journal_mode=MEMORY;
|
||||
PRAGMA page_size=8192; -- Optional, but recommended
|
||||
```
|
||||
|
||||
### 6. Development Workflow
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
yarn add @jlongster/sql.js absurd-sql
|
||||
```
|
||||
|
||||
2. Development commands:
|
||||
- `yarn build` - Build the project
|
||||
- `yarn jest` - Run tests
|
||||
- `yarn serve` - Start development server
|
||||
|
||||
### 7. Testing Guidelines
|
||||
- Write tests for both SharedArrayBuffer and fallback modes
|
||||
- Use Jest for testing
|
||||
- Include performance benchmarks for critical operations
|
||||
|
||||
### 8. Performance Considerations
|
||||
- Use bulk operations when possible
|
||||
- Monitor read/write performance
|
||||
- Consider using transactions for multiple operations
|
||||
- Avoid unnecessary database connections
|
||||
|
||||
### 9. Error Handling
|
||||
- Implement proper error handling for:
|
||||
- Worker initialization failures
|
||||
- Database connection issues
|
||||
- Concurrent access conflicts (in fallback mode)
|
||||
- Storage quota exceeded scenarios
|
||||
|
||||
### 10. Security Best Practices
|
||||
- Never expose database operations directly to the client
|
||||
- Validate all SQL queries
|
||||
- Implement proper access controls
|
||||
- Handle sensitive data appropriately
|
||||
|
||||
### 11. Code Style
|
||||
- Follow ESLint configuration
|
||||
- Use async/await for asynchronous operations
|
||||
- Document complex database operations
|
||||
- Include comments for non-obvious optimizations
|
||||
|
||||
### 12. Debugging
|
||||
- Use `jest-debug` for debugging tests
|
||||
- Monitor IndexedDB usage in browser dev tools
|
||||
- Check worker communication in console
|
||||
- Use performance monitoring tools
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Worker Initialization
|
||||
```javascript
|
||||
// Main thread
|
||||
import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread';
|
||||
|
||||
function init() {
|
||||
let worker = new Worker(new URL('./index.worker.js', import.meta.url));
|
||||
initBackend(worker);
|
||||
}
|
||||
```
|
||||
|
||||
### Database Setup
|
||||
```javascript
|
||||
// Worker thread
|
||||
import initSqlJs from '@jlongster/sql.js';
|
||||
import { SQLiteFS } from 'absurd-sql';
|
||||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
|
||||
|
||||
async function setupDatabase() {
|
||||
let SQL = await initSqlJs({ locateFile: file => file });
|
||||
let sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
|
||||
SQL.register_for_idb(sqlFS);
|
||||
|
||||
SQL.FS.mkdir('/sql');
|
||||
SQL.FS.mount(sqlFS, {}, '/sql');
|
||||
|
||||
return new SQL.Database('/sql/db.sqlite', { filename: true });
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. SharedArrayBuffer not available
|
||||
- Check COOP/COEP headers
|
||||
- Verify browser support
|
||||
- Test fallback mode
|
||||
|
||||
2. Worker initialization failures
|
||||
- Check file paths
|
||||
- Verify module imports
|
||||
- Check browser console for errors
|
||||
|
||||
3. Performance issues
|
||||
- Monitor IndexedDB usage
|
||||
- Check for unnecessary operations
|
||||
- Verify transaction usage
|
||||
|
||||
## Resources
|
||||
- [Project Demo](https://priceless-keller-d097e5.netlify.app/)
|
||||
- [Example Project](https://github.com/jlongster/absurd-example-project)
|
||||
- [Blog Post](https://jlongster.com/future-sql-web)
|
||||
- [SQL.js Documentation](https://github.com/sql-js/sql.js/)
|
||||
292
.cursor/rules/architectural_decision_record.mdc
Normal file
@@ -0,0 +1,292 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# TimeSafari Cross-Platform Architecture Guide
|
||||
|
||||
## 1. Platform Support Matrix
|
||||
|
||||
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) | PyWebView (Desktop) |
|
||||
|---------|-----------|-------------------|-------------------|-------------------|
|
||||
| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented |
|
||||
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented | Not Implemented |
|
||||
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs | PyWebView Python Bridge |
|
||||
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented | Not Implemented |
|
||||
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks | process.env checks |
|
||||
|
||||
## 2. Project Structure
|
||||
|
||||
### 2.1 Core Directories
|
||||
```
|
||||
src/
|
||||
├── components/ # Vue components
|
||||
├── services/ # Platform services and business logic
|
||||
├── views/ # Page components
|
||||
├── router/ # Vue router configuration
|
||||
├── types/ # TypeScript type definitions
|
||||
├── utils/ # Utility functions
|
||||
├── lib/ # Core libraries
|
||||
├── platforms/ # Platform-specific implementations
|
||||
├── electron/ # Electron-specific code
|
||||
├── constants/ # Application constants
|
||||
├── db/ # Database related code
|
||||
├── interfaces/ # TypeScript interfaces and type definitions
|
||||
└── assets/ # Static assets
|
||||
```
|
||||
|
||||
### 2.2 Entry Points
|
||||
```
|
||||
src/
|
||||
├── main.ts # Base entry
|
||||
├── main.common.ts # Shared initialization
|
||||
├── main.capacitor.ts # Mobile entry
|
||||
├── main.electron.ts # Electron entry
|
||||
├── main.pywebview.ts # PyWebView entry
|
||||
└── main.web.ts # Web/PWA entry
|
||||
```
|
||||
|
||||
### 2.3 Build Configurations
|
||||
```
|
||||
root/
|
||||
├── vite.config.common.mts # Shared config
|
||||
├── vite.config.capacitor.mts # Mobile build
|
||||
├── vite.config.electron.mts # Electron build
|
||||
├── vite.config.pywebview.mts # PyWebView build
|
||||
├── vite.config.web.mts # Web/PWA build
|
||||
└── vite.config.utils.mts # Build utilities
|
||||
```
|
||||
|
||||
## 3. Service Architecture
|
||||
|
||||
### 3.1 Service Organization
|
||||
```
|
||||
services/
|
||||
├── QRScanner/ # QR code scanning service
|
||||
│ ├── WebInlineQRScanner.ts
|
||||
│ └── interfaces.ts
|
||||
├── platforms/ # Platform-specific services
|
||||
│ ├── WebPlatformService.ts
|
||||
│ ├── CapacitorPlatformService.ts
|
||||
│ ├── ElectronPlatformService.ts
|
||||
│ └── PyWebViewPlatformService.ts
|
||||
└── factory/ # Service factories
|
||||
└── PlatformServiceFactory.ts
|
||||
```
|
||||
|
||||
### 3.2 Service Factory Pattern
|
||||
```typescript
|
||||
// PlatformServiceFactory.ts
|
||||
export class PlatformServiceFactory {
|
||||
private static instance: PlatformService | null = null;
|
||||
|
||||
public static getInstance(): PlatformService {
|
||||
if (!PlatformServiceFactory.instance) {
|
||||
const platform = process.env.VITE_PLATFORM || "web";
|
||||
PlatformServiceFactory.instance = createPlatformService(platform);
|
||||
}
|
||||
return PlatformServiceFactory.instance;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Feature Implementation Guidelines
|
||||
|
||||
### 4.1 QR Code Scanning
|
||||
|
||||
1. **Service Interface**
|
||||
```typescript
|
||||
interface QRScannerService {
|
||||
checkPermissions(): Promise<boolean>;
|
||||
requestPermissions(): Promise<boolean>;
|
||||
isSupported(): Promise<boolean>;
|
||||
startScan(): Promise<void>;
|
||||
stopScan(): Promise<void>;
|
||||
addListener(listener: ScanListener): void;
|
||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Platform-Specific Implementation**
|
||||
```typescript
|
||||
// WebInlineQRScanner.ts
|
||||
export class WebInlineQRScanner implements QRScannerService {
|
||||
private scanListener: ScanListener | null = null;
|
||||
private isScanning = false;
|
||||
private stream: MediaStream | null = null;
|
||||
private events = new EventEmitter();
|
||||
|
||||
// Implementation of interface methods
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Deep Linking
|
||||
|
||||
1. **URL Structure**
|
||||
```typescript
|
||||
// Format: timesafari://<route>[/<param>][?queryParam1=value1]
|
||||
interface DeepLinkParams {
|
||||
route: string;
|
||||
params?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Platform Handlers**
|
||||
```typescript
|
||||
// Capacitor
|
||||
App.addListener("appUrlOpen", handleDeepLink);
|
||||
|
||||
// Web
|
||||
router.beforeEach((to, from, next) => {
|
||||
handleWebDeepLink(to.query);
|
||||
});
|
||||
```
|
||||
|
||||
## 5. Build Process
|
||||
|
||||
### 5.1 Environment Configuration
|
||||
```typescript
|
||||
// vite.config.common.mts
|
||||
export function createBuildConfig(mode: string) {
|
||||
return {
|
||||
define: {
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative),
|
||||
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||
__USE_QR_READER__: JSON.stringify(!isCapacitor)
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Platform-Specific Builds
|
||||
|
||||
```bash
|
||||
# Build commands from package.json
|
||||
"build:web": "vite build --config vite.config.web.mts",
|
||||
"build:capacitor": "vite build --config vite.config.capacitor.mts",
|
||||
"build:electron": "vite build --config vite.config.electron.mts",
|
||||
"build:pywebview": "vite build --config vite.config.pywebview.mts"
|
||||
```
|
||||
|
||||
## 6. Testing Strategy
|
||||
|
||||
### 6.1 Test Configuration
|
||||
```typescript
|
||||
// playwright.config-local.ts
|
||||
const config: PlaywrightTestConfig = {
|
||||
projects: [
|
||||
{
|
||||
name: 'web',
|
||||
use: { browserName: 'chromium' }
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
use: { ...devices['Pixel 5'] }
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 Platform-Specific Tests
|
||||
```typescript
|
||||
test('QR scanning works on mobile', async ({ page }) => {
|
||||
test.skip(!process.env.MOBILE_TEST, 'Mobile-only test');
|
||||
// Test implementation
|
||||
});
|
||||
```
|
||||
|
||||
## 7. Error Handling
|
||||
|
||||
### 7.1 Global Error Handler
|
||||
```typescript
|
||||
function setupGlobalErrorHandler(app: VueApp) {
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
logger.error("[App Error]", {
|
||||
error: err,
|
||||
info,
|
||||
component: instance?.$options.name
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Platform-Specific Error Handling
|
||||
```typescript
|
||||
// API error handling for Capacitor
|
||||
if (process.env.VITE_PLATFORM === 'capacitor') {
|
||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||
message: error.message,
|
||||
status: error.response?.status
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Best Practices
|
||||
|
||||
### 8.1 Code Organization
|
||||
- Use platform-specific directories for unique implementations
|
||||
- Share common code through service interfaces
|
||||
- Implement feature detection before using platform capabilities
|
||||
- Keep platform-specific code isolated in dedicated directories
|
||||
- Use TypeScript interfaces for cross-platform compatibility
|
||||
|
||||
### 8.2 Platform Detection
|
||||
```typescript
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const capabilities = platformService.getCapabilities();
|
||||
|
||||
if (capabilities.hasCamera) {
|
||||
// Implement camera features
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Feature Implementation
|
||||
1. Define platform-agnostic interface
|
||||
2. Create platform-specific implementations
|
||||
3. Use factory pattern for instantiation
|
||||
4. Implement graceful fallbacks
|
||||
5. Add comprehensive error handling
|
||||
6. Use dependency injection for better testability
|
||||
|
||||
## 9. Dependency Management
|
||||
|
||||
### 9.1 Platform-Specific Dependencies
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"electron": "^33.2.1",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 Conditional Loading
|
||||
```typescript
|
||||
if (process.env.VITE_PLATFORM === 'capacitor') {
|
||||
await import('@capacitor/core');
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Security Considerations
|
||||
|
||||
### 10.1 Permission Handling
|
||||
```typescript
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
if (platformService.isCapacitor()) {
|
||||
return await checkNativePermissions();
|
||||
}
|
||||
return await checkWebPermissions();
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Data Storage
|
||||
- Use secure storage mechanisms for sensitive data
|
||||
- Implement proper encryption for stored data
|
||||
- Follow platform-specific security guidelines
|
||||
- Regular security audits and updates
|
||||
|
||||
This document should be updated as new features are added or platform-specific implementations change. Regular reviews ensure it remains current with the codebase.
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Camera Implementation Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes how camera functionality is implemented across the TimeSafari application. The application uses cameras for two main purposes:
|
||||
|
||||
1. QR Code scanning
|
||||
2. Photo capture
|
||||
|
||||
## Components
|
||||
|
||||
### QRScannerDialog.vue
|
||||
|
||||
Primary component for QR code scanning in web browsers.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Uses `qrcode-stream` for web-based QR scanning
|
||||
- Supports both front and back cameras
|
||||
- Provides real-time camera status feedback
|
||||
- Implements error handling with user-friendly messages
|
||||
- Includes camera switching functionality
|
||||
|
||||
**Camera Access Flow:**
|
||||
|
||||
1. Checks for camera API availability
|
||||
2. Enumerates available video devices
|
||||
3. Requests camera permissions
|
||||
4. Initializes camera stream with preferred settings
|
||||
5. Handles various error conditions with specific messages
|
||||
|
||||
### PhotoDialog.vue
|
||||
|
||||
Component for photo capture and selection.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Cross-platform photo capture interface
|
||||
- Image cropping capabilities
|
||||
- File selection fallback
|
||||
- Unified interface for different platforms
|
||||
|
||||
## Services
|
||||
|
||||
### QRScanner Services
|
||||
|
||||
#### WebDialogQRScanner
|
||||
|
||||
Web-based implementation of QR scanning.
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `checkPermissions()`: Verifies camera permission status
|
||||
- `requestPermissions()`: Requests camera access
|
||||
- `isSupported()`: Checks for camera API support
|
||||
- Handles various error conditions with specific messages
|
||||
|
||||
#### CapacitorQRScanner
|
||||
|
||||
Native implementation using Capacitor's MLKit.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Uses `@capacitor-mlkit/barcode-scanning`
|
||||
- Supports both front and back cameras
|
||||
- Implements permission management
|
||||
- Provides continuous scanning capability
|
||||
|
||||
### Platform Services
|
||||
|
||||
#### WebPlatformService
|
||||
|
||||
Web-specific implementation of platform features.
|
||||
|
||||
**Camera Capabilities:**
|
||||
|
||||
- Uses HTML5 file input with capture attribute
|
||||
- Falls back to file selection if camera unavailable
|
||||
- Processes captured images for consistent format
|
||||
|
||||
#### CapacitorPlatformService
|
||||
|
||||
Native implementation using Capacitor.
|
||||
|
||||
**Camera Features:**
|
||||
|
||||
- Uses `Camera.getPhoto()` for native camera access
|
||||
- Supports image editing
|
||||
- Configures high-quality image capture
|
||||
- Handles base64 image processing
|
||||
|
||||
#### ElectronPlatformService
|
||||
|
||||
Desktop implementation (currently unimplemented).
|
||||
|
||||
**Status:**
|
||||
|
||||
- Camera functionality not yet implemented
|
||||
- Planned to use Electron's media APIs
|
||||
|
||||
## Platform-Specific Considerations
|
||||
|
||||
### iOS
|
||||
|
||||
- Requires `NSCameraUsageDescription` in Info.plist
|
||||
- Supports both front and back cameras
|
||||
- Implements proper permission handling
|
||||
|
||||
### Android
|
||||
|
||||
- Requires camera permissions in manifest
|
||||
- Supports both front and back cameras
|
||||
- Handles permission requests through Capacitor
|
||||
|
||||
### Web
|
||||
|
||||
- Requires HTTPS for camera access
|
||||
- Implements fallback mechanisms
|
||||
- Handles browser compatibility issues
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Scenarios
|
||||
|
||||
1. No camera found
|
||||
2. Permission denied
|
||||
3. Camera in use by another application
|
||||
4. HTTPS required
|
||||
5. Browser compatibility issues
|
||||
|
||||
### Error Response
|
||||
|
||||
- User-friendly error messages
|
||||
- Troubleshooting tips
|
||||
- Clear instructions for resolution
|
||||
- Platform-specific guidance
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Permission Management
|
||||
|
||||
- Explicit permission requests
|
||||
- Permission state tracking
|
||||
- Graceful handling of denied permissions
|
||||
|
||||
### Data Handling
|
||||
|
||||
- Secure image processing
|
||||
- Proper cleanup of camera resources
|
||||
- No persistent storage of camera data
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Camera Access
|
||||
|
||||
1. Always check for camera availability
|
||||
2. Request permissions explicitly
|
||||
3. Handle all error conditions
|
||||
4. Provide clear user feedback
|
||||
5. Implement proper cleanup
|
||||
|
||||
### Performance
|
||||
|
||||
1. Optimize camera resolution
|
||||
2. Implement proper resource cleanup
|
||||
3. Handle camera switching efficiently
|
||||
4. Manage memory usage
|
||||
|
||||
### User Experience
|
||||
|
||||
1. Clear status indicators
|
||||
2. Intuitive camera controls
|
||||
3. Helpful error messages
|
||||
4. Smooth camera switching
|
||||
5. Responsive UI feedback
|
||||
|
||||
## Future Improvements
|
||||
|
||||
### Planned Enhancements
|
||||
|
||||
1. Implement Electron camera support
|
||||
2. Add advanced camera features
|
||||
3. Improve error handling
|
||||
4. Enhance user feedback
|
||||
5. Optimize performance
|
||||
|
||||
### Known Issues
|
||||
|
||||
1. Electron camera implementation pending
|
||||
2. Some browser compatibility limitations
|
||||
3. Platform-specific quirks to address
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Key Packages
|
||||
|
||||
- `@capacitor-mlkit/barcode-scanning`
|
||||
- `qrcode-stream`
|
||||
- `vue-picture-cropper`
|
||||
- Platform-specific camera APIs
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
1. Permission handling
|
||||
2. Camera switching
|
||||
3. Error conditions
|
||||
4. Platform compatibility
|
||||
5. Performance metrics
|
||||
|
||||
### Test Environment
|
||||
|
||||
- Multiple browsers
|
||||
- iOS and Android devices
|
||||
- Desktop platforms
|
||||
- Various network conditions
|
||||
276
.cursor/rules/timesafari.mdc
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Time Safari Context
|
||||
|
||||
## Project Overview
|
||||
|
||||
Time Safari is an application designed to foster community building through gifts, gratitude, and collaborative projects. The app should make it extremely easy and intuitive for users of any age and capability to recognize contributions, build trust networks, and organize collective action. It is built on services that preserve privacy and data sovereignty.
|
||||
|
||||
The ultimate goals of Time Safari are two-fold:
|
||||
|
||||
1. **Connect** Make it easy, rewarding, and non-threatening for people to connect with others who have similar interests, and to initiate activities together. This helps people accomplish and learn from other individuals in less-structured environments; moreover, it helps them discover who they want to continue to support and with whom they want to maintain relationships.
|
||||
|
||||
2. **Reveal** Widely advertise the great support and rewards that are being given and accepted freely, especially non-monetary ones. Using visuals and text, display the kind of impact that gifts are making in the lives of others. Also show useful and engaging reports of project statistics and personal accomplishments.
|
||||
|
||||
|
||||
## Core Approaches
|
||||
|
||||
Time Safari should help everyday users build meaningful connections and organize collective efforts by:
|
||||
|
||||
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts and contributions people give to each other and their communities.
|
||||
|
||||
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask for or propose help on projects and interests that matter to them.
|
||||
|
||||
3. **Building Trust Networks**: Enabling users to maintain their network and activity visibility. Developing reputation through verified contributions and references, which can be selectively shown to others outside the network.
|
||||
|
||||
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with explicitly authorized contacts, allowing private individuals including children to participate safely.
|
||||
|
||||
5. **Engaging Content**: Displaying people's records in compelling stories, and highlighting those projects that are lifting people's lives long-term, both in physical support and in emotional-spiritual-creative thriving.
|
||||
|
||||
|
||||
## Technical Foundation
|
||||
|
||||
This application is built on a privacy-preserving claims architecture (via endorser.ch) with these key characteristics:
|
||||
|
||||
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private key pairs stored on their devices
|
||||
- **Cryptographic Verification**: All claims and confirmations are cryptographically signed
|
||||
- **User-Controlled Visibility**: Users explicitly control who can see their identifiers and data
|
||||
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification and integrity
|
||||
- **Native and Web App**: Works on Capacitor (iOS, Android), Desktop (Electron and CEFPython), and web browsers
|
||||
|
||||
## User Journey
|
||||
|
||||
The typical progression of usage follows these stages:
|
||||
|
||||
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude for gifts received, building a foundation of acknowledgment.
|
||||
|
||||
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect with others who share similar interests.
|
||||
|
||||
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute proposed projects, moving from ideas to action.
|
||||
|
||||
## Context for LLM Development
|
||||
|
||||
When developing new functionality for Time Safari, consider these design principles:
|
||||
|
||||
1. **Accessibility First**: Features should be usable by non-technical users with minimal learning curve.
|
||||
|
||||
2. **Privacy by Design**: All features must respect user privacy and data sovereignty.
|
||||
|
||||
3. **Progressive Enhancement**: Core functionality should work across all devices, with richer experiences where supported.
|
||||
|
||||
4. **Voluntary Collaboration**: The system should enable but never coerce participation.
|
||||
|
||||
5. **Trust Building**: Features should help build verifiable trust between users.
|
||||
|
||||
6. **Network Effects**: Consider how features scale as more users join the platform.
|
||||
|
||||
7. **Low Resource Requirements**: The system should be lightweight enough to run on inexpensive devices users already own.
|
||||
|
||||
## Use Cases to Support
|
||||
|
||||
LLM development should focus on enhancing these key use cases:
|
||||
|
||||
1. **Community Building**: Tools that help people find others with shared interests and values.
|
||||
|
||||
2. **Project Coordination**: Features that make it easy to propose collaborative projects and to submit suggestions and offers to existing ones.
|
||||
|
||||
3. **Reputation Building**: Methods for users to showcase their contributions and reliability, in contexts where they explicitly reveal that information.
|
||||
|
||||
4. **Governance Experimentation**: Features that facilitate decision-making and collective governance.
|
||||
|
||||
## Constraints
|
||||
|
||||
When developing new features, be mindful of these constraints:
|
||||
|
||||
1. **Privacy Preservation**: User identifiers must remain private except when explicitly shared.
|
||||
|
||||
2. **Platform Limitations**: Features must work within the constraints of the target app platforms, while aiming to leverage the best platform technology available.
|
||||
|
||||
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch API capabilities.
|
||||
|
||||
4. **Performance on Low-End Devices**: The application should remain performant on older/simpler devices.
|
||||
|
||||
5. **Offline-First When Possible**: Key functionality should work offline when feasible.
|
||||
|
||||
## Project Technologies
|
||||
|
||||
- Typescript using ES6 classes using vue-facing-decorator
|
||||
- TailwindCSS
|
||||
- Vite Build Tool
|
||||
- Playwright E2E testing
|
||||
- IndexDB
|
||||
- Camera, Image uploads, QR Code reader, ...
|
||||
|
||||
## Mobile Features
|
||||
|
||||
- Deep Linking
|
||||
- Local Notifications via a custom Capacitor plugin
|
||||
|
||||
## Project Architecture
|
||||
|
||||
- The application must work on web browser, PWA (Progressive Web Application), desktop via Electron, and mobile via Capacitor
|
||||
- Building for each platform is managed via Vite
|
||||
|
||||
## Core Development Principles
|
||||
|
||||
### DRY development
|
||||
- **Code Reuse**
|
||||
- Extract common functionality into utility functions
|
||||
- Create reusable components for UI patterns
|
||||
- Implement service classes for shared business logic
|
||||
- Use mixins for cross-cutting concerns
|
||||
- Leverage TypeScript interfaces for shared type definitions
|
||||
|
||||
- **Component Patterns**
|
||||
- Create base components for common UI elements
|
||||
- Implement higher-order components for shared behavior
|
||||
- Use slot patterns for flexible component composition
|
||||
- Create composable services for business logic
|
||||
- Implement factory patterns for component creation
|
||||
|
||||
- **State Management**
|
||||
- Centralize state in Pinia stores
|
||||
- Use computed properties for derived state
|
||||
- Implement shared state selectors
|
||||
- Create reusable state mutations
|
||||
- Use action creators for common operations
|
||||
|
||||
- **Error Handling**
|
||||
- Implement centralized error handling
|
||||
- Create reusable error components
|
||||
- Use error boundary components
|
||||
- Implement consistent error logging
|
||||
- Create error type definitions
|
||||
|
||||
- **Type Definitions**
|
||||
- Create shared interfaces for common data structures
|
||||
- Use type aliases for complex types
|
||||
- Implement generic types for reusable components
|
||||
- Create utility types for common patterns
|
||||
- Use discriminated unions for state management
|
||||
|
||||
- **API Integration**
|
||||
- Create reusable API client classes
|
||||
- Implement request/response interceptors
|
||||
- Use consistent error handling patterns
|
||||
- Create type-safe API endpoints
|
||||
- Implement caching strategies
|
||||
|
||||
- **Platform Services**
|
||||
- Abstract platform-specific code behind interfaces
|
||||
- Create platform-agnostic service layers
|
||||
- Implement feature detection
|
||||
- Use dependency injection for services
|
||||
- Create service factories
|
||||
|
||||
- **Testing**
|
||||
- Create reusable test utilities
|
||||
- Implement test factories
|
||||
- Use shared test configurations
|
||||
- Create reusable test helpers
|
||||
- Implement consistent test patterns
|
||||
|
||||
### SOLID Principles
|
||||
- **Single Responsibility**: Each class/component should have only one reason to change
|
||||
- Components should focus on one specific feature (e.g., QR scanning, DID management)
|
||||
- Services should handle one type of functionality (e.g., platform services, crypto services)
|
||||
- Utilities should provide focused helper functions
|
||||
|
||||
- **Open/Closed**: Software entities should be open for extension but closed for modification
|
||||
- Use interfaces for service definitions
|
||||
- Implement plugin architecture for platform-specific features
|
||||
- Allow component behavior extension through props and events
|
||||
|
||||
- **Liskov Substitution**: Objects should be replaceable with their subtypes
|
||||
- Platform services should work consistently across web/mobile
|
||||
- Authentication providers should be interchangeable
|
||||
- Storage implementations should be swappable
|
||||
|
||||
- **Interface Segregation**: Clients shouldn't depend on interfaces they don't use
|
||||
- Break down large service interfaces into smaller, focused ones
|
||||
- Component props should be minimal and purposeful
|
||||
- Event emissions should be specific and targeted
|
||||
|
||||
- **Dependency Inversion**: High-level modules shouldn't depend on low-level modules
|
||||
- Use dependency injection for services
|
||||
- Abstract platform-specific code behind interfaces
|
||||
- Implement factory patterns for component creation
|
||||
|
||||
### Law of Demeter
|
||||
- Components should only communicate with immediate dependencies
|
||||
- Avoid chaining method calls (e.g., `this.service.getUser().getProfile().getName()`)
|
||||
- Use mediator patterns for complex component interactions
|
||||
- Implement facade patterns for subsystem access
|
||||
- Keep component communication through defined events and props
|
||||
|
||||
### Composition over Inheritance
|
||||
- Prefer building components through composition
|
||||
- Use mixins for shared functionality
|
||||
- Implement feature toggles through props
|
||||
- Create higher-order components for common patterns
|
||||
- Use service composition for complex features
|
||||
|
||||
### Interface Segregation
|
||||
- Define clear interfaces for services
|
||||
- Keep component APIs minimal and focused
|
||||
- Split large interfaces into smaller, specific ones
|
||||
- Use TypeScript interfaces for type definitions
|
||||
- Implement role-based interfaces for different use cases
|
||||
|
||||
### Fail Fast
|
||||
- Validate inputs early in the process
|
||||
- Use TypeScript strict mode
|
||||
- Implement comprehensive error handling
|
||||
- Add runtime checks for critical operations
|
||||
- Use assertions for development-time validation
|
||||
|
||||
### Principle of Least Astonishment
|
||||
- Follow Vue.js conventions consistently
|
||||
- Use familiar naming patterns
|
||||
- Implement predictable component behaviors
|
||||
- Maintain consistent error handling
|
||||
- Keep UI interactions intuitive
|
||||
|
||||
### Information Hiding
|
||||
- Encapsulate implementation details
|
||||
- Use private class members
|
||||
- Implement proper access modifiers
|
||||
- Hide complex logic behind simple interfaces
|
||||
- Use TypeScript's access modifiers effectively
|
||||
|
||||
### Single Source of Truth
|
||||
- Use Pinia for state management
|
||||
- Maintain one source for user data
|
||||
- Centralize configuration management
|
||||
- Use computed properties for derived state
|
||||
- Implement proper state synchronization
|
||||
|
||||
### Principle of Least Privilege
|
||||
- Implement proper access control
|
||||
- Use minimal required permissions
|
||||
- Follow privacy-by-design principles
|
||||
- Restrict component access to necessary data
|
||||
- Implement proper authentication/authorization
|
||||
|
||||
### Continuous Integration/Continuous Deployment (CI/CD)
|
||||
- Automated testing on every commit
|
||||
- Consistent build process across platforms
|
||||
- Automated deployment pipelines
|
||||
- Quality gates for code merging
|
||||
- Environment-specific configurations
|
||||
|
||||
This expanded documentation provides:
|
||||
1. Clear principles for development
|
||||
2. Practical implementation guidelines
|
||||
3. Real-world examples
|
||||
4. TypeScript integration
|
||||
5. Best practices for Time Safari
|
||||
|
||||
267
.cursor/rules/wa-sqlite.mdc
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# wa-sqlite Usage Guide
|
||||
|
||||
## Table of Contents
|
||||
- [1. Overview](#1-overview)
|
||||
- [2. Installation](#2-installation)
|
||||
- [3. Basic Setup](#3-basic-setup)
|
||||
- [3.1 Import and Initialize](#31-import-and-initialize)
|
||||
- [3.2 Basic Database Operations](#32-basic-database-operations)
|
||||
- [4. Virtual File Systems (VFS)](#4-virtual-file-systems-vfs)
|
||||
- [4.1 Available VFS Options](#41-available-vfs-options)
|
||||
- [4.2 Using a VFS](#42-using-a-vfs)
|
||||
- [5. Best Practices](#5-best-practices)
|
||||
- [5.1 Error Handling](#51-error-handling)
|
||||
- [5.2 Transaction Management](#52-transaction-management)
|
||||
- [5.3 Prepared Statements](#53-prepared-statements)
|
||||
- [6. Performance Considerations](#6-performance-considerations)
|
||||
- [7. Common Issues and Solutions](#7-common-issues-and-solutions)
|
||||
- [8. TypeScript Support](#8-typescript-support)
|
||||
|
||||
## 1. Overview
|
||||
wa-sqlite is a WebAssembly build of SQLite that enables SQLite database operations in web browsers and JavaScript environments. It provides both synchronous and asynchronous builds, with support for custom virtual file systems (VFS) for persistent storage.
|
||||
|
||||
## 2. Installation
|
||||
```bash
|
||||
npm install wa-sqlite
|
||||
# or
|
||||
yarn add wa-sqlite
|
||||
```
|
||||
|
||||
## 3. Basic Setup
|
||||
|
||||
### 3.1 Import and Initialize
|
||||
```javascript
|
||||
// Choose one of these imports based on your needs:
|
||||
// - wa-sqlite.mjs: Synchronous build
|
||||
// - wa-sqlite-async.mjs: Asynchronous build (required for async VFS)
|
||||
// - wa-sqlite-jspi.mjs: JSPI-based async build (experimental, Chromium only)
|
||||
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs';
|
||||
import * as SQLite from 'wa-sqlite';
|
||||
|
||||
async function initDatabase() {
|
||||
// Initialize SQLite module
|
||||
const module = await SQLiteESMFactory();
|
||||
const sqlite3 = SQLite.Factory(module);
|
||||
|
||||
// Open database (returns a Promise)
|
||||
const db = await sqlite3.open_v2('myDatabase');
|
||||
return { sqlite3, db };
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Basic Database Operations
|
||||
```javascript
|
||||
async function basicOperations() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
// Create a table
|
||||
await sqlite3.exec(db, `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE
|
||||
)
|
||||
`);
|
||||
|
||||
// Insert data
|
||||
await sqlite3.exec(db, `
|
||||
INSERT INTO users (name, email)
|
||||
VALUES ('John Doe', 'john@example.com')
|
||||
`);
|
||||
|
||||
// Query data
|
||||
const results = [];
|
||||
await sqlite3.exec(db, 'SELECT * FROM users', (row, columns) => {
|
||||
results.push({ row, columns });
|
||||
});
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
// Always close the database when done
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Virtual File Systems (VFS)
|
||||
|
||||
### 4.1 Available VFS Options
|
||||
wa-sqlite provides several VFS implementations for persistent storage:
|
||||
|
||||
1. **IDBBatchAtomicVFS** (Recommended for general use)
|
||||
- Uses IndexedDB with batch atomic writes
|
||||
- Works in all contexts (Window, Worker, Service Worker)
|
||||
- Supports WAL mode
|
||||
- Best performance with `PRAGMA synchronous=normal`
|
||||
|
||||
2. **IDBMirrorVFS**
|
||||
- Keeps files in memory, persists to IndexedDB
|
||||
- Works in all contexts
|
||||
- Good for smaller databases
|
||||
|
||||
3. **OPFS-based VFS** (Origin Private File System)
|
||||
- Various implementations available:
|
||||
- AccessHandlePoolVFS
|
||||
- OPFSAdaptiveVFS
|
||||
- OPFSCoopSyncVFS
|
||||
- OPFSPermutedVFS
|
||||
- Better performance but limited to Worker contexts
|
||||
|
||||
### 4.2 Using a VFS
|
||||
```javascript
|
||||
import { IDBBatchAtomicVFS } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js';
|
||||
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs';
|
||||
import * as SQLite from 'wa-sqlite';
|
||||
|
||||
async function initDatabaseWithVFS() {
|
||||
const module = await SQLiteESMFactory();
|
||||
const sqlite3 = SQLite.Factory(module);
|
||||
|
||||
// Register VFS
|
||||
const vfs = await IDBBatchAtomicVFS.create('myApp', module);
|
||||
sqlite3.vfs_register(vfs, true);
|
||||
|
||||
// Open database with VFS
|
||||
const db = await sqlite3.open_v2('myDatabase');
|
||||
|
||||
// Configure for better performance
|
||||
await sqlite3.exec(db, 'PRAGMA synchronous = normal');
|
||||
await sqlite3.exec(db, 'PRAGMA journal_mode = WAL');
|
||||
|
||||
return { sqlite3, db };
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Best Practices
|
||||
|
||||
### 5.1 Error Handling
|
||||
```javascript
|
||||
async function safeDatabaseOperation() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
await sqlite3.exec(db, 'SELECT * FROM non_existent_table');
|
||||
} catch (error) {
|
||||
if (error.code === SQLite.SQLITE_ERROR) {
|
||||
console.error('SQL error:', error.message);
|
||||
} else {
|
||||
console.error('Database error:', error);
|
||||
}
|
||||
} finally {
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Transaction Management
|
||||
```javascript
|
||||
async function transactionExample() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
await sqlite3.exec(db, 'BEGIN TRANSACTION');
|
||||
|
||||
// Perform multiple operations
|
||||
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Alice']);
|
||||
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Bob']);
|
||||
|
||||
await sqlite3.exec(db, 'COMMIT');
|
||||
} catch (error) {
|
||||
await sqlite3.exec(db, 'ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Prepared Statements
|
||||
```javascript
|
||||
async function preparedStatementExample() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
// Prepare statement
|
||||
const stmt = await sqlite3.prepare(db, 'SELECT * FROM users WHERE id = ?');
|
||||
|
||||
// Execute with different parameters
|
||||
await sqlite3.bind(stmt, 1, 1);
|
||||
while (await sqlite3.step(stmt) === SQLite.SQLITE_ROW) {
|
||||
const row = sqlite3.row(stmt);
|
||||
console.log(row);
|
||||
}
|
||||
|
||||
// Reset and reuse
|
||||
await sqlite3.reset(stmt);
|
||||
await sqlite3.bind(stmt, 1, 2);
|
||||
// ... execute again
|
||||
|
||||
await sqlite3.finalize(stmt);
|
||||
} finally {
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Performance Considerations
|
||||
|
||||
1. **VFS Selection**
|
||||
- Use IDBBatchAtomicVFS for general-purpose applications
|
||||
- Consider OPFS-based VFS for better performance in Worker contexts
|
||||
- Use MemoryVFS for temporary databases
|
||||
|
||||
2. **Configuration**
|
||||
- Set appropriate page size (default is usually fine)
|
||||
- Use WAL mode for better concurrency
|
||||
- Consider `PRAGMA synchronous=normal` for better performance
|
||||
- Adjust cache size based on your needs
|
||||
|
||||
3. **Concurrency**
|
||||
- Use transactions for multiple operations
|
||||
- Be aware of VFS-specific concurrency limitations
|
||||
- Consider using Web Workers for heavy database operations
|
||||
|
||||
## 7. Common Issues and Solutions
|
||||
|
||||
1. **Database Locking**
|
||||
- Use appropriate transaction isolation levels
|
||||
- Implement retry logic for busy errors
|
||||
- Consider using WAL mode
|
||||
|
||||
2. **Storage Limitations**
|
||||
- Be aware of browser storage quotas
|
||||
- Implement cleanup strategies
|
||||
- Monitor database size
|
||||
|
||||
3. **Cross-Context Access**
|
||||
- Use appropriate VFS for your context
|
||||
- Consider message passing for cross-context communication
|
||||
- Be aware of storage access limitations
|
||||
|
||||
## 8. TypeScript Support
|
||||
wa-sqlite includes TypeScript definitions. The main types are:
|
||||
|
||||
```typescript
|
||||
type SQLiteCompatibleType = number | string | Uint8Array | Array<number> | bigint | null;
|
||||
|
||||
interface SQLiteAPI {
|
||||
open_v2(filename: string, flags?: number, zVfs?: string): Promise<number>;
|
||||
exec(db: number, sql: string, callback?: (row: any[], columns: string[]) => void): Promise<number>;
|
||||
close(db: number): Promise<number>;
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Official GitHub Repository](https://github.com/rhashimoto/wa-sqlite)
|
||||
- [Online Demo](https://rhashimoto.github.io/wa-sqlite/demo/)
|
||||
- [API Reference](https://rhashimoto.github.io/wa-sqlite/docs/)
|
||||
- [FAQ](https://github.com/rhashimoto/wa-sqlite/issues?q=is%3Aissue+label%3Afaq+)
|
||||
- [Discussion Forums](https://github.com/rhashimoto/wa-sqlite/discussions)
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
# iOS doesn't like spaces in the app title.
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
|
||||
VITE_APP_SERVER=http://localhost:3000
|
||||
VITE_APP_SERVER=http://localhost:8080
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
# Using shared server by default to ease setup, which works for shared test users.
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Admin DID credentials
|
||||
ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
|
||||
ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b
|
||||
|
||||
# API Configuration
|
||||
ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
|
||||
@@ -9,3 +9,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||
|
||||
@@ -9,4 +9,5 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -4,6 +4,12 @@ module.exports = {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'dist-electron/',
|
||||
'*.d.ts'
|
||||
],
|
||||
extends: [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
@@ -26,6 +32,7 @@ module.exports = {
|
||||
"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-unnecessary-type-constraint": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||
},
|
||||
};
|
||||
|
||||
19
.gitignore
vendored
@@ -38,30 +38,21 @@ pnpm-debug.log*
|
||||
/dist-capacitor/
|
||||
/test-playwright-results/
|
||||
playwright-tests
|
||||
test-playwright
|
||||
dist-electron-packages
|
||||
ios
|
||||
.ruby-version
|
||||
+.env
|
||||
|
||||
# Generated test files
|
||||
# Test files generated by scripts test-ios.js & test-android.js
|
||||
.generated/
|
||||
|
||||
# Fastlane
|
||||
ios/fastlane/report.xml
|
||||
ios/fastlane/Preview.html
|
||||
ios/fastlane/screenshots
|
||||
ios/fastlane/test_output
|
||||
android/fastlane/report.xml
|
||||
android/fastlane/Preview.html
|
||||
android/fastlane/screenshots
|
||||
android/fastlane/test_output
|
||||
.env.default
|
||||
vendor/
|
||||
|
||||
# Build logs
|
||||
build_logs/
|
||||
|
||||
# Android generated assets
|
||||
android/app/src/main/assets/public/assets/
|
||||
# PWA icon files generated by capacitor-assets
|
||||
icons
|
||||
|
||||
|
||||
android/app/src/main/res/
|
||||
605
BUILDING.md
@@ -9,8 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
||||
- Node.js (LTS version recommended)
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
- For iOS builds: macOS with Xcode installed
|
||||
- For Android builds: Android Studio with SDK installed
|
||||
- For desktop builds: Additional build tools based on your OS
|
||||
|
||||
## Forks
|
||||
@@ -22,44 +20,191 @@ If you have forked this to make your own app, you'll want to customize the iOS &
|
||||
npx cap add ios
|
||||
```
|
||||
|
||||
You'll also want to edit the deep link configuration.
|
||||
You'll also want to edit the deep link configuration (see below).
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone [repository-url]
|
||||
cd TimeSafari
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Web Build
|
||||
## Web Dev Locally
|
||||
|
||||
To build for web deployment:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Web Build for Server
|
||||
|
||||
1. Run the production build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
rm -rf dist
|
||||
npm run build:web
|
||||
```
|
||||
|
||||
2. The built files will be in the `dist` directory.
|
||||
The built files will be in the `dist` directory.
|
||||
|
||||
3. To test the production build locally:
|
||||
2. To test the production build locally:
|
||||
|
||||
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compile and minify for test & production
|
||||
|
||||
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
|
||||
|
||||
* `npx prettier --write ./sw_scripts/`
|
||||
|
||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||
|
||||
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
|
||||
|
||||
* Commit everything (since the commit hash is used the app).
|
||||
|
||||
* Put the commit hash in the changelog (which will help you remember to bump the version in the step later).
|
||||
|
||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.2 && git push origin 1.0.2`.
|
||||
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```bash
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web
|
||||
```
|
||||
|
||||
... and transfer to the test server:
|
||||
|
||||
```bash
|
||||
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
|
||||
```
|
||||
|
||||
(Let's replace that with a .env.development or .env.staging file.)
|
||||
|
||||
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
|
||||
|
||||
* For prod, get on the server and run the correct build:
|
||||
|
||||
... and log onto the server:
|
||||
|
||||
* `pkgx +npm sh`
|
||||
|
||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web && cd -`
|
||||
|
||||
(The plain `npm run build:web` uses the .env.production file.)
|
||||
|
||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-2 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||
|
||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production.
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
The application can be containerized using Docker for consistent deployment across environments.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker installed on your system
|
||||
- Docker Compose (optional, for multi-container setups)
|
||||
|
||||
### Building the Docker Image
|
||||
|
||||
1. Build the Docker image:
|
||||
|
||||
```bash
|
||||
docker build -t timesafari:latest .
|
||||
```
|
||||
|
||||
2. For development builds with specific environment variables:
|
||||
|
||||
```bash
|
||||
docker build --build-arg NODE_ENV=development -t timesafari:dev .
|
||||
```
|
||||
|
||||
### Running the Container
|
||||
|
||||
1. Run the container:
|
||||
|
||||
```bash
|
||||
docker run -d -p 80:80 timesafari:latest
|
||||
```
|
||||
|
||||
2. For development with hot-reloading:
|
||||
|
||||
```bash
|
||||
docker run -d -p 80:80 -v $(pwd):/app timesafari:dev
|
||||
```
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
Create a `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
timesafari:
|
||||
build: .
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Run with Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
|
||||
For production deployment, consider the following:
|
||||
|
||||
1. Use specific version tags instead of 'latest'
|
||||
2. Implement health checks
|
||||
3. Configure proper logging
|
||||
4. Set up reverse proxy with SSL termination
|
||||
5. Use Docker secrets for sensitive data
|
||||
|
||||
Example production deployment:
|
||||
|
||||
```bash
|
||||
# Build with specific version
|
||||
docker build -t timesafari:1.0.0 .
|
||||
|
||||
# Run with production settings
|
||||
docker run -d \
|
||||
--name timesafari \
|
||||
-p 80:80 \
|
||||
--restart unless-stopped \
|
||||
-e NODE_ENV=production \
|
||||
timesafari:1.0.0
|
||||
```
|
||||
|
||||
### Troubleshooting Docker
|
||||
|
||||
1. **Container fails to start**
|
||||
- Check logs: `docker logs <container_id>`
|
||||
- Verify port availability
|
||||
- Check environment variables
|
||||
|
||||
2. **Build fails**
|
||||
- Ensure all dependencies are in package.json
|
||||
- Check Dockerfile syntax
|
||||
- Verify build context
|
||||
|
||||
3. **Performance issues**
|
||||
- Monitor container resources: `docker stats`
|
||||
- Check nginx configuration
|
||||
- Verify caching settings
|
||||
|
||||
## Desktop Build (Electron)
|
||||
|
||||
### Building for Linux
|
||||
### Linux Build
|
||||
|
||||
1. Build the electron app in production mode:
|
||||
|
||||
@@ -81,21 +226,77 @@ To build for web deployment:
|
||||
- AppImage: `dist-electron-packages/TimeSafari-x.x.x.AppImage`
|
||||
- DEB: `dist-electron-packages/timesafari_x.x.x_amd64.deb`
|
||||
|
||||
### macOS Build
|
||||
|
||||
1. Build the electron app in production mode:
|
||||
|
||||
```bash
|
||||
npm run build:web
|
||||
npm run build:electron
|
||||
npm run electron:build-mac
|
||||
```
|
||||
|
||||
2. Package the Electron app for macOS:
|
||||
|
||||
```bash
|
||||
# For Intel Macs
|
||||
npm run electron:build-mac
|
||||
|
||||
# For Universal build (Intel + Apple Silicon)
|
||||
npm run electron:build-mac-universal
|
||||
```
|
||||
|
||||
3. The packaged applications will be in `dist-electron-packages/`:
|
||||
- `.app` bundle: `TimeSafari.app`
|
||||
- `.dmg` installer: `TimeSafari-x.x.x.dmg`
|
||||
- `.zip` archive: `TimeSafari-x.x.x-mac.zip`
|
||||
|
||||
### Code Signing and Notarization (macOS)
|
||||
|
||||
For public distribution on macOS, you need to code sign and notarize your app:
|
||||
|
||||
1. Set up environment variables:
|
||||
```bash
|
||||
export CSC_LINK=/path/to/your/certificate.p12
|
||||
export CSC_KEY_PASSWORD=your_certificate_password
|
||||
export APPLE_ID=your_apple_id
|
||||
export APPLE_ID_PASSWORD=your_app_specific_password
|
||||
```
|
||||
|
||||
2. Build with signing:
|
||||
```bash
|
||||
npm run electron:build-mac
|
||||
```
|
||||
|
||||
### Running the Packaged App
|
||||
|
||||
- AppImage: Make executable and run
|
||||
- **Linux**:
|
||||
- AppImage: Make executable and run
|
||||
```bash
|
||||
chmod +x dist-electron-packages/TimeSafari-*.AppImage
|
||||
./dist-electron-packages/TimeSafari-*.AppImage
|
||||
```
|
||||
- DEB: Install and run
|
||||
```bash
|
||||
sudo dpkg -i dist-electron-packages/timesafari_*_amd64.deb
|
||||
timesafari
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod +x dist-electron-packages/TimeSafari-*.AppImage
|
||||
./dist-electron-packages/TimeSafari-*.AppImage
|
||||
```
|
||||
- **macOS**:
|
||||
- `.app` bundle: Double-click `TimeSafari.app` in Finder
|
||||
- `.dmg` installer:
|
||||
1. Double-click the `.dmg` file
|
||||
2. Drag the app to your Applications folder
|
||||
3. Launch from Applications
|
||||
- `.zip` archive:
|
||||
1. Extract the `.zip` file
|
||||
2. Move `TimeSafari.app` to your Applications folder
|
||||
3. Launch from Applications
|
||||
|
||||
- DEB: Install and run
|
||||
|
||||
```bash
|
||||
sudo dpkg -i dist-electron-packages/timesafari_*_amd64.deb
|
||||
timesafari
|
||||
```
|
||||
Note: If you get a security warning when running the app:
|
||||
1. Right-click the app
|
||||
2. Select "Open"
|
||||
3. Click "Open" in the security dialog
|
||||
|
||||
### Development Testing
|
||||
|
||||
@@ -115,36 +316,84 @@ npm run build:electron-prod && npm run electron:start
|
||||
|
||||
Prerequisites: macOS with Xcode installed
|
||||
|
||||
1. Build the web assets:
|
||||
#### First-time iOS Configuration
|
||||
|
||||
- Generate certificates inside XCode.
|
||||
|
||||
- Right-click on App and under Signing & Capabilities set the Team.
|
||||
|
||||
#### Each Release
|
||||
|
||||
0. First time (or if dependencies change):
|
||||
|
||||
- `pkgx +rubygems.org sh`
|
||||
|
||||
- ... and you may have to fix these, especially with pkgx:
|
||||
|
||||
```bash
|
||||
npm run build:capacitor
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
|
||||
2. Update iOS project with latest build:
|
||||
1. Build the web assets & update ios:
|
||||
|
||||
```bash
|
||||
rm -rf dist
|
||||
npm run build:web
|
||||
npm run build:capacitor
|
||||
npx cap sync ios
|
||||
```
|
||||
|
||||
- If that fails with "Could not find..." then look at the "gem_path" instructions above.
|
||||
|
||||
3. Copy the assets:
|
||||
|
||||
```bash
|
||||
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
|
||||
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
|
||||
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
|
||||
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
|
||||
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
|
||||
npx capacitor-assets generate --ios
|
||||
```
|
||||
|
||||
3. Open the project in Xcode:
|
||||
4. Bump the version to match Android & package.json:
|
||||
|
||||
```
|
||||
cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
```
|
||||
|
||||
5. Open the project in Xcode:
|
||||
|
||||
```bash
|
||||
npx cap open ios
|
||||
```
|
||||
|
||||
4. Use Xcode to build and run on simulator or device.
|
||||
6. Use Xcode to build and run on simulator or device.
|
||||
|
||||
* Select Product -> Destination with some Simulator version. Then click the run arrow.
|
||||
|
||||
7. Release
|
||||
|
||||
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
||||
* Choose Product -> Destination -> Any iOS Device
|
||||
* Choose Product -> Archive
|
||||
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
|
||||
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
|
||||
* Click Distribute -> App Store Connect
|
||||
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
|
||||
* May have to go to App Review, click Submission, then hover over the build and click "-".
|
||||
* It can take 15 minutes for the build to show up in the list of builds.
|
||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
||||
|
||||
### Android Build
|
||||
|
||||
Prerequisites: Android Studio with SDK installed
|
||||
Prerequisites: Android Studio with Java SDK installed
|
||||
|
||||
1. Build the web assets:
|
||||
|
||||
@@ -166,32 +415,57 @@ Prerequisites: Android Studio with SDK installed
|
||||
npx capacitor-assets generate --android
|
||||
```
|
||||
|
||||
4. Open the project in Android Studio:
|
||||
4. Bump version to match iOS & package.json: android/app/build.gradle
|
||||
|
||||
5. Open the project in Android Studio:
|
||||
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
5. Use Android Studio to build and run on emulator or device.
|
||||
6. Use Android Studio to build and run on emulator or device.
|
||||
|
||||
## Building Android from the console
|
||||
## Android Build from the console
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew clean
|
||||
./gradlew build -Dlint.baselines.continue=true
|
||||
cd ..
|
||||
npx cap run android
|
||||
cd -
|
||||
```
|
||||
|
||||
... or, to create the `aab` file, `bundle` instead of `build`:
|
||||
|
||||
```bash
|
||||
./gradlew bundle -Dlint.baselines.continue=true
|
||||
./gradlew bundleDebug -Dlint.baselines.continue=true
|
||||
```
|
||||
|
||||
... or, to create a signed release:
|
||||
|
||||
## Configuring Android for deep links
|
||||
* Setup by adding the app/gradle.properties.secrets file (see properties at top of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file
|
||||
* In app/build.gradle, bump the versionCode and maybe the versionName
|
||||
* Then `bundleRelease`:
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
cd -
|
||||
```
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
|
||||
At play.google.com/console:
|
||||
|
||||
- Go to the Testing Track (eg. Closed).
|
||||
- Click "Create new release".
|
||||
- Upload the `aab` file.
|
||||
- Hit "Next".
|
||||
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
|
||||
|
||||
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
|
||||
|
||||
|
||||
## Android Configuration for deep links
|
||||
|
||||
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
||||
|
||||
@@ -204,251 +478,4 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
|
||||
</intent-filter>
|
||||
```
|
||||
|
||||
You must also add the following to the `android/app/build.gradle` file:
|
||||
|
||||
```gradle
|
||||
android {
|
||||
// ... existing config ...
|
||||
|
||||
lintOptions {
|
||||
disable 'UnsanitizedFilenameFromContentProvider'
|
||||
abortOnError false
|
||||
baseline file("lint-baseline.xml")
|
||||
|
||||
// Ignore Capacitor module issues
|
||||
ignore 'DefaultLocale'
|
||||
ignore 'UnsanitizedFilenameFromContentProvider'
|
||||
ignore 'LintBaseline'
|
||||
ignore 'LintBaselineFixed'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Modify `/android/build.gradle` to use a stable version of AGP and make sure Kotlin version is compatible.
|
||||
|
||||
```gradle
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
// Use a stable version of AGP
|
||||
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||
|
||||
// Make sure Kotlin version is compatible
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
// Add this to handle version conflicts
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
|
||||
force 'org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PyWebView Desktop Build
|
||||
|
||||
### Prerequisites for PyWebView
|
||||
|
||||
- Python 3.8 or higher
|
||||
- pip (Python package manager)
|
||||
- virtualenv (recommended)
|
||||
- System dependencies:
|
||||
|
||||
```bash
|
||||
# For Ubuntu/Debian
|
||||
sudo apt-get install python3-webview
|
||||
# or
|
||||
sudo apt-get install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0
|
||||
|
||||
# For Arch Linux
|
||||
sudo pacman -S webkit2gtk python-gobject python-cairo
|
||||
|
||||
# For Fedora
|
||||
sudo dnf install python3-webview
|
||||
# or
|
||||
sudo dnf install python3-gobject python3-cairo webkit2gtk3
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
1. Create and activate a virtual environment (recommended):
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Linux/macOS
|
||||
# or
|
||||
.venv\Scripts\activate # On Windows
|
||||
```
|
||||
|
||||
2. Install Python dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If encountering PyInstaller version errors:
|
||||
|
||||
```bash
|
||||
# Try installing the latest stable version
|
||||
pip install --upgrade pyinstaller
|
||||
```
|
||||
|
||||
### Development of PyWebView
|
||||
|
||||
1. Start the PyWebView development build:
|
||||
|
||||
```bash
|
||||
npm run pywebview:dev
|
||||
```
|
||||
|
||||
### Building for Distribution
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
npm run pywebview:package-linux
|
||||
```
|
||||
|
||||
The packaged application will be in `dist/TimeSafari`
|
||||
|
||||
#### Windows
|
||||
|
||||
```bash
|
||||
npm run pywebview:package-win
|
||||
```
|
||||
|
||||
The packaged application will be in `dist/TimeSafari`
|
||||
|
||||
#### macOS
|
||||
|
||||
```bash
|
||||
npm run pywebview:package-mac
|
||||
```
|
||||
|
||||
The packaged application will be in `dist/TimeSafari`
|
||||
|
||||
## Testing
|
||||
|
||||
Run all tests (requires XCode and Android Studio/device):
|
||||
|
||||
```bash
|
||||
npm run test:all
|
||||
```
|
||||
|
||||
See [TESTING.md](test-playwright/TESTING.md) for more details.
|
||||
|
||||
## Linting
|
||||
|
||||
Check code style:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Fix code style issues:
|
||||
|
||||
```bash
|
||||
npm run lint-fix
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
See `.env.*` files for configuration.
|
||||
|
||||
## Notes
|
||||
|
||||
- The application uses PWA (Progressive Web App) features for web builds
|
||||
- Electron builds disable PWA features automatically
|
||||
- Build output directories:
|
||||
- Web: `dist/`
|
||||
- Electron: `dist-electron/`
|
||||
- Capacitor: `dist-capacitor/`
|
||||
|
||||
## Deployment
|
||||
|
||||
### Version Management
|
||||
|
||||
1. Update CHANGELOG.md with new changes
|
||||
2. Update version in package.json
|
||||
3. Commit changes and tag release:
|
||||
|
||||
```bash
|
||||
git tag <VERSION_TAG>
|
||||
git push origin <VERSION_TAG>
|
||||
```
|
||||
|
||||
4. After deployment, update package.json with next version + "-beta"
|
||||
|
||||
### Test Server
|
||||
|
||||
```bash
|
||||
# Build using staging environment
|
||||
npm run build -- --mode staging
|
||||
|
||||
# Deploy to test server
|
||||
rsync -azvu -e "ssh -i ~/.ssh/<YOUR_KEY>" dist ubuntutest@test.timesafari.app:time-safari/
|
||||
```
|
||||
|
||||
### Production Server
|
||||
|
||||
```bash
|
||||
# On the production server:
|
||||
pkgx +npm sh
|
||||
cd crowd-funder-for-time-pwa
|
||||
git checkout master && git pull
|
||||
git checkout <VERSION_TAG>
|
||||
npm install
|
||||
npm run build
|
||||
cd -
|
||||
|
||||
# Backup and deploy
|
||||
mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/
|
||||
```
|
||||
|
||||
## Troubleshooting Builds
|
||||
|
||||
### Common Build Issues
|
||||
|
||||
1. **Missing Environment Variables**
|
||||
- Check that all required variables are set in your .env file
|
||||
- For development, ensure local services are running on correct ports
|
||||
|
||||
2. **Electron Build Failures**
|
||||
- Verify Node.js version compatibility
|
||||
- Check that all required dependencies are installed
|
||||
- Ensure proper paths in electron/main.js
|
||||
|
||||
3. **Mobile Build Issues**
|
||||
- For iOS: Xcode command line tools must be installed
|
||||
- For Android: Correct SDK version must be installed
|
||||
- Check Capacitor configuration in capacitor.config.ts
|
||||
|
||||
|
||||
# List all installed packages
|
||||
adb shell pm list packages | grep timesafari
|
||||
|
||||
# Force stop the app (if it's running)
|
||||
adb shell am force-stop app.timesafari
|
||||
|
||||
# Clear app data (if you don't want to fully uninstall)
|
||||
adb shell pm clear app.timesafari
|
||||
|
||||
# Uninstall for all users
|
||||
adb shell pm uninstall -k --user 0 app.timesafari
|
||||
|
||||
# Check if app is installed
|
||||
adb shell pm path app.timesafari
|
||||
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
|
||||
|
||||
34
CHANGELOG.md
@@ -6,6 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- Photo is pinned to profile mode
|
||||
|
||||
|
||||
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
||||
### Added
|
||||
- Version on feed title
|
||||
|
||||
|
||||
## [1.0.1] - 2025.06.20
|
||||
### Added
|
||||
- Allow a user to block someone else's content from view
|
||||
|
||||
|
||||
## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73
|
||||
### Added
|
||||
- Web-oriented migration from IndexedDB to SQLite
|
||||
|
||||
|
||||
## [0.5.8]
|
||||
### Added
|
||||
- /deep-link/ path for URLs that are shared with people
|
||||
### Changed
|
||||
- External links now go to /deep-link/...
|
||||
- Feed visuals now have arrow imagery from giver to receiver
|
||||
|
||||
|
||||
## [0.4.7]
|
||||
### Fixed
|
||||
- Cameras everywhere
|
||||
### Changed
|
||||
- IndexedDB -> SQLite
|
||||
|
||||
|
||||
## [0.4.5] - 2025.02.23
|
||||
### Added
|
||||
|
||||
36
Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
# Build stage
|
||||
FROM node:22-alpine3.20 AS builder
|
||||
|
||||
# Install build dependencies
|
||||
|
||||
RUN apk add --no-cache bash git python3 py3-pip py3-setuptools make g++ gcc
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build:web
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built assets from builder stage
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration if needed
|
||||
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
124
README.md
@@ -3,6 +3,32 @@
|
||||
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
|
||||
and expand to crowd-fund with time & money, then record and see the impact of contributions.
|
||||
|
||||
## Database Migration Status
|
||||
|
||||
**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
|
||||
|
||||
### Migration Progress
|
||||
- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
|
||||
- ✅ **Platform Service Layer**: Unified database interface across platforms
|
||||
- ✅ **Settings Migration**: Core user settings transferred
|
||||
- ✅ **Account Migration**: Identity and key management
|
||||
- 🔄 **Contact Migration**: User contact data (via import interface)
|
||||
- 📋 **Code Cleanup**: Remove unused Dexie imports
|
||||
|
||||
### Migration Fence
|
||||
The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
|
||||
|
||||
**Key Points**:
|
||||
- Legacy Dexie database is disabled by default (`USE_DEXIE_DB = false`)
|
||||
- All database operations go through `PlatformService`
|
||||
- Migration tools provide controlled access to both databases
|
||||
- Clear separation between legacy and new code
|
||||
|
||||
### Migration Documentation
|
||||
- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
|
||||
- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
|
||||
- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
|
||||
|
||||
## Roadmap
|
||||
|
||||
See [project.task.yaml](project.task.yaml) for current priorities.
|
||||
@@ -12,6 +38,8 @@ See [project.task.yaml](project.task.yaml) for current priorities.
|
||||
|
||||
Quick start:
|
||||
|
||||
* For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
@@ -19,72 +47,15 @@ npm run dev
|
||||
|
||||
See [BUILDING.md](BUILDING.md) for more details.
|
||||
|
||||
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
|
||||
|
||||
|
||||
### Run all UI tests
|
||||
|
||||
Look at [BUILDING.md](BUILDING.md) for the "test-all" instructions and [TESTING.md](test-playwright/TESTING.md) for more details.
|
||||
|
||||
|
||||
### Compile and minify for test & production
|
||||
|
||||
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
|
||||
|
||||
* `npx prettier --write ./sw_scripts/`
|
||||
|
||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||
|
||||
* Commit everything (since the commit hash is used the app).
|
||||
|
||||
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
||||
|
||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
|
||||
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```bash
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||
```
|
||||
|
||||
... and transfer to the test server:
|
||||
|
||||
```bash
|
||||
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
|
||||
```
|
||||
|
||||
(Let's replace that with a .env.development or .env.staging file.)
|
||||
|
||||
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
|
||||
|
||||
* For prod, get on the server and run the correct build:
|
||||
|
||||
... and log onto the server:
|
||||
|
||||
* `pkgx +npm sh`
|
||||
|
||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
|
||||
|
||||
(The plain `npm run build` uses the .env.production file.)
|
||||
|
||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||
|
||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
||||
|
||||
|
||||
|
||||
|
||||
## Icons
|
||||
|
||||
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
||||
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
||||
|
||||
To add a Font Awesome icon, add to main.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
|
||||
## Other
|
||||
|
||||
@@ -97,6 +68,39 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
||||
|
||||
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||
|
||||
### Code Organization
|
||||
|
||||
The project uses a centralized approach to type definitions and interfaces:
|
||||
|
||||
* `src/interfaces/` - Contains all TypeScript interfaces and type definitions
|
||||
* `deepLinks.ts` - Deep linking type system and Zod validation schemas
|
||||
* `give.ts` - Give-related interfaces and type definitions
|
||||
* `claims.ts` - Claim-related interfaces and verifiable credentials
|
||||
* `common.ts` - Shared interfaces and utility types
|
||||
* Other domain-specific interface files
|
||||
|
||||
Key principles:
|
||||
- All interfaces and types are defined in the interfaces folder
|
||||
- Zod schemas are used for runtime validation and type generation
|
||||
- Domain-specific interfaces are separated into their own files
|
||||
- Common interfaces are shared through `common.ts`
|
||||
- Type definitions are generated from Zod schemas where possible
|
||||
|
||||
### Database Architecture
|
||||
|
||||
The application uses a platform-agnostic database layer:
|
||||
|
||||
* `src/services/PlatformService.ts` - Database interface definition
|
||||
* `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
|
||||
* `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
|
||||
* `src/db/` - Legacy Dexie database (migration in progress)
|
||||
|
||||
**Development Guidelines**:
|
||||
- Always use `PlatformService` for database operations
|
||||
- Never import Dexie directly in application code
|
||||
- Test with `USE_DEXIE_DB = false` for new features
|
||||
- Use migration tools for data transfer between systems
|
||||
|
||||
### Kudos
|
||||
|
||||
Gifts make the world go 'round!
|
||||
|
||||
84
TASK_storage.md
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
# What to do about storage for native apps?
|
||||
|
||||
|
||||
## Problem
|
||||
|
||||
We can't trust iOS IndexedDB to persist. I want to start delivering an app to people now, in preparation for presentations mid-June: Rotary on June 12 and Porcfest on June 17.
|
||||
|
||||
* Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/).
|
||||
|
||||
* The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well.
|
||||
|
||||
* Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage).
|
||||
|
||||
Also, with sensitive data, the accounts info should be encrypted.
|
||||
|
||||
|
||||
# Options
|
||||
|
||||
* There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher).
|
||||
|
||||
* [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native.
|
||||
|
||||
* Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.)
|
||||
|
||||
* There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API.
|
||||
|
||||
* [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption.
|
||||
|
||||
* Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0).
|
||||
|
||||
|
||||
|
||||
# Current Plan
|
||||
|
||||
* Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users.
|
||||
|
||||
* After that is delivered, write a migration for current web users from IndexedDB to SQLite.
|
||||
|
||||
|
||||
|
||||
# Current method calls
|
||||
|
||||
... which is not 100% complete because the AI that generated thus claimed no usage of 'temp' DB.
|
||||
|
||||
### Secret Database (secretDB) - Used for storing the encryption key
|
||||
|
||||
secretDB.open() - Opens the database
|
||||
secretDB.secret.get(MASTER_SECRET_KEY) - Retrieves the secret key
|
||||
secretDB.secret.add({ id: MASTER_SECRET_KEY, secret }) - Adds a new secret key
|
||||
|
||||
### Accounts Database (accountsDB) - Used for storing sensitive account information
|
||||
|
||||
accountsDB.open() - Opens the database
|
||||
accountsDB.accounts.count() - Counts number of accounts
|
||||
accountsDB.accounts.toArray() - Gets all accounts
|
||||
accountsDB.accounts.where("did").equals(did).first() - Gets a specific account by DID
|
||||
accountsDB.accounts.add(account) - Adds a new account
|
||||
|
||||
### Non-sensitive Database (db) - Used for settings, contacts, logs, and temp data
|
||||
|
||||
Settings operations:
|
||||
export all settings (Dexie format)
|
||||
db.settings.get(MASTER_SETTINGS_KEY) - Gets default settings
|
||||
db.settings.where("accountDid").equals(did).first() - Gets account-specific settings
|
||||
db.settings.where("accountDid").equals(did).modify(settingsChanges) - Updates account settings
|
||||
db.settings.add(settingsChanges) - Adds new settings
|
||||
db.settings.count() - Counts number of settings
|
||||
db.settings.update(key, changes) - Updates settings
|
||||
|
||||
Contacts operations:
|
||||
export all contacts (Dexie format)
|
||||
db.contacts.toArray() - Gets all contacts
|
||||
db.contacts.add(contact) - Adds a new contact
|
||||
db.contacts.update(did, contactData) - Updates a contact
|
||||
db.contacts.delete(did) - Deletes a contact
|
||||
db.contacts.where("did").equals(did).first() - Gets a specific contact by DID
|
||||
|
||||
Logs operations:
|
||||
db.logs.get(todayKey) - Gets logs for a specific day
|
||||
db.logs.update(todayKey, { message: fullMessage }) - Updates logs
|
||||
db.logs.clear() - Clears all logs
|
||||
|
||||
|
||||
23
android/.gitignore
vendored
@@ -1,5 +1,20 @@
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
app/build/*
|
||||
!app/build/.npmkeep
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
|
||||
# secrets
|
||||
app/gradle.properties.secrets
|
||||
app/time-safari-upload-key-pkcs12.jks
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
@@ -91,11 +106,3 @@ lint/tmp/
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#Fri Mar 21 07:27:50 UTC 2025
|
||||
gradle.version=8.2.1
|
||||
2
android/app/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -1,14 +1,38 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
// These are sample values to set in gradle.properties.secrets
|
||||
// MY_KEYSTORE_FILE=time-safari-upload-key-pkcs12.jks
|
||||
// MY_KEYSTORE_PASSWORD=...
|
||||
// MY_KEY_ALIAS=time-safari-key-alias
|
||||
// MY_KEY_PASSWORD=...
|
||||
|
||||
// Try to load from environment variables first
|
||||
project.ext.MY_KEYSTORE_FILE = System.getenv('ANDROID_KEYSTORE_FILE') ?: ""
|
||||
project.ext.MY_KEYSTORE_PASSWORD = System.getenv('ANDROID_KEYSTORE_PASSWORD') ?: ""
|
||||
project.ext.MY_KEY_ALIAS = System.getenv('ANDROID_KEY_ALIAS') ?: ""
|
||||
project.ext.MY_KEY_PASSWORD = System.getenv('ANDROID_KEY_PASSWORD') ?: ""
|
||||
|
||||
// If no environment variables, try to load from secrets file
|
||||
if (!project.ext.MY_KEYSTORE_FILE) {
|
||||
def secretsPropertiesFile = rootProject.file("app/gradle.properties.secrets")
|
||||
if (secretsPropertiesFile.exists()) {
|
||||
Properties secretsProperties = new Properties()
|
||||
secretsProperties.load(new FileInputStream(secretsPropertiesFile))
|
||||
secretsProperties.each { name, value ->
|
||||
project.ext[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'app.timesafari'
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "app.timesafari"
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 35
|
||||
versionName "1.0.2"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -16,10 +40,41 @@ android {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
if (project.ext.MY_KEYSTORE_FILE &&
|
||||
project.ext.MY_KEYSTORE_PASSWORD &&
|
||||
project.ext.MY_KEY_ALIAS &&
|
||||
project.ext.MY_KEY_PASSWORD) {
|
||||
|
||||
storeFile file(project.ext.MY_KEYSTORE_FILE)
|
||||
storePassword project.ext.MY_KEYSTORE_PASSWORD
|
||||
keyAlias project.ext.MY_KEY_ALIAS
|
||||
keyPassword project.ext.MY_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
// Only sign if we have the signing config
|
||||
if (signingConfigs.release.storeFile != null) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = true
|
||||
}
|
||||
density {
|
||||
enableSplit = true
|
||||
}
|
||||
abi {
|
||||
enableSplit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +91,8 @@ dependencies {
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
|
||||
@@ -9,7 +9,13 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capawesome-capacitor-file-picker')
|
||||
|
||||
}
|
||||
|
||||
|
||||
28
android/app/google-services.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "123456789000",
|
||||
"project_id": "timesafari-app",
|
||||
"storage_bucket": "timesafari-app.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:123456789000:android:1234567890abcdef",
|
||||
"android_client_info": {
|
||||
"package_name": "app.timesafari.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDummyKeyForBuildPurposesOnly12345"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("app.timesafari", appContext.getPackageName());
|
||||
assertEquals("app.timesafari.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -8,15 +7,14 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_main"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -28,7 +26,6 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="timesafari" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
@@ -36,13 +33,15 @@
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||
</manifest>
|
||||
|
||||
@@ -16,6 +16,41 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"SQLite": {
|
||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||
"iosIsEncryption": true,
|
||||
"iosBiometric": {
|
||||
"biometricAuth": true,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
},
|
||||
"androidIsEncryption": true,
|
||||
"androidBiometric": {
|
||||
"biometricAuth": true,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"contentInset": "never",
|
||||
"allowsLinkPreview": true,
|
||||
"scrollEnabled": true,
|
||||
"limitsNavigationsToAppBoundDomains": true,
|
||||
"backgroundColor": "#ffffff",
|
||||
"allowNavigation": [
|
||||
"*.timesafari.app",
|
||||
"*.jsdelivr.net",
|
||||
"api.endorser.ch"
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": false,
|
||||
"captureInput": true,
|
||||
"webContentsDebuggingEnabled": false,
|
||||
"allowNavigation": [
|
||||
"*.timesafari.app",
|
||||
"*.jsdelivr.net",
|
||||
"api.endorser.ch"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
[
|
||||
{
|
||||
"pkg": "@capacitor-community/sqlite",
|
||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor-mlkit/barcode-scanning",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/app",
|
||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/camera",
|
||||
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/filesystem",
|
||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capawesome/capacitor-file-picker",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>TimeSafari</title>
|
||||
<script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +1,15 @@
|
||||
package app.timesafari;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
//import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
// ... existing code ...
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Initialize SQLite
|
||||
//registerPlugin(SQLite.class);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package timesafari.app;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -2,4 +2,5 @@
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
<files-path name="my_files" path="." />
|
||||
</paths>
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -2,5 +2,23 @@
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-community-sqlite'
|
||||
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
||||
|
||||
include ':capacitor-mlkit-barcode-scanning'
|
||||
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
||||
|
||||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||
|
||||
include ':capacitor-camera'
|
||||
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
|
||||
|
||||
include ':capacitor-filesystem'
|
||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
include ':capawesome-capacitor-file-picker'
|
||||
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
assets/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
Application icons are here. They are processed for android & ios by the `capacitor-assets` command, as indicated in the BUILDING.md file.
|
||||
|
Before Width: | Height: | Size: 190 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/splash-dark.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/splash.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
4
build.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
export IMAGENAME="$(basename $PWD):1.0"
|
||||
|
||||
docker build . --network=host -t $IMAGENAME --no-cache
|
||||
56
capacitor.config.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"appId": "app.timesafari",
|
||||
"appName": "TimeSafari",
|
||||
"webDir": "dist",
|
||||
"bundledWebRuntime": false,
|
||||
"server": {
|
||||
"cleartext": true
|
||||
},
|
||||
"plugins": {
|
||||
"App": {
|
||||
"appUrlOpen": {
|
||||
"handlers": [
|
||||
{
|
||||
"url": "timesafari://*",
|
||||
"autoVerify": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"SQLite": {
|
||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||
"iosIsEncryption": true,
|
||||
"iosBiometric": {
|
||||
"biometricAuth": true,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
},
|
||||
"androidIsEncryption": true,
|
||||
"androidBiometric": {
|
||||
"biometricAuth": true,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"contentInset": "never",
|
||||
"allowsLinkPreview": true,
|
||||
"scrollEnabled": true,
|
||||
"limitsNavigationsToAppBoundDomains": true,
|
||||
"backgroundColor": "#ffffff",
|
||||
"allowNavigation": [
|
||||
"*.timesafari.app",
|
||||
"*.jsdelivr.net",
|
||||
"api.endorser.ch"
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": false,
|
||||
"captureInput": true,
|
||||
"webContentsDebuggingEnabled": false,
|
||||
"allowNavigation": [
|
||||
"*.timesafari.app",
|
||||
"*.jsdelivr.net",
|
||||
"api.endorser.ch"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'app.timesafari',
|
||||
appName: 'TimeSafari',
|
||||
webDir: 'dist',
|
||||
bundledWebRuntime: false,
|
||||
server: {
|
||||
cleartext: true,
|
||||
},
|
||||
plugins: {
|
||||
App: {
|
||||
appUrlOpen: {
|
||||
handlers: [
|
||||
{
|
||||
url: "timesafari://*",
|
||||
autoVerify: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -9,23 +9,98 @@ The deep linking system uses a multi-layered type safety approach:
|
||||
- Enforces parameter requirements
|
||||
- Sanitizes input data
|
||||
- Provides detailed validation errors
|
||||
- Generates TypeScript types automatically
|
||||
|
||||
2. **TypeScript Types**
|
||||
- Generated from Zod schemas
|
||||
- Generated from Zod schemas using `z.infer`
|
||||
- Ensures compile-time type safety
|
||||
- Provides IDE autocompletion
|
||||
- Catches type errors during development
|
||||
- Maintains single source of truth for types
|
||||
|
||||
3. **Router Integration**
|
||||
- Type-safe parameter passing
|
||||
- Route-specific parameter validation
|
||||
- Query parameter type checking
|
||||
- Automatic type inference for route parameters
|
||||
|
||||
## Type System Implementation
|
||||
|
||||
### Zod Schema to TypeScript Type Generation
|
||||
|
||||
```typescript
|
||||
// Define the schema
|
||||
const claimSchema = z.object({
|
||||
id: z.string(),
|
||||
view: z.enum(["details", "certificate", "raw"]).optional()
|
||||
});
|
||||
|
||||
// TypeScript type is automatically generated
|
||||
type ClaimParams = z.infer<typeof claimSchema>;
|
||||
// Equivalent to:
|
||||
// type ClaimParams = {
|
||||
// id: string;
|
||||
// view?: "details" | "certificate" | "raw";
|
||||
// }
|
||||
```
|
||||
|
||||
### Type Safety Layers
|
||||
|
||||
1. **Schema Definition**
|
||||
```typescript
|
||||
// src/interfaces/deepLinks.ts
|
||||
export const deepLinkSchemas = {
|
||||
claim: z.object({
|
||||
id: z.string(),
|
||||
view: z.enum(["details", "certificate", "raw"]).optional()
|
||||
}),
|
||||
// Other route schemas...
|
||||
};
|
||||
```
|
||||
|
||||
2. **Type Generation**
|
||||
```typescript
|
||||
// Types are automatically generated from schemas
|
||||
export type DeepLinkParams = {
|
||||
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
||||
};
|
||||
```
|
||||
|
||||
3. **Runtime Validation**
|
||||
```typescript
|
||||
// In DeepLinkHandler
|
||||
const result = deepLinkSchemas.claim.safeParse(params);
|
||||
if (!result.success) {
|
||||
// Handle validation errors
|
||||
console.error(result.error);
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Types
|
||||
|
||||
```typescript
|
||||
export interface DeepLinkError extends Error {
|
||||
code: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
// Usage in error handling
|
||||
try {
|
||||
await handler.handleDeepLink(url);
|
||||
} catch (error) {
|
||||
if (error instanceof DeepLinkError) {
|
||||
// Type-safe error handling
|
||||
console.error(error.code, error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Files
|
||||
|
||||
- `src/types/deepLinks.ts`: Type definitions and validation schemas
|
||||
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
||||
- `src/services/deepLinks.ts`: Deep link processing service
|
||||
- `src/main.capacitor.ts`: Capacitor integration
|
||||
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||
|
||||
## Type Safety Examples
|
||||
|
||||
295
doc/database-migration-guide.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Database Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The Database Migration feature allows you to compare and migrate data between Dexie (IndexedDB) and SQLite databases in the TimeSafari application. This is particularly useful during the transition from the old Dexie-based storage system to the new SQLite-based system.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Database Comparison
|
||||
|
||||
- Compare data between Dexie and SQLite databases
|
||||
- View detailed differences in contacts and settings
|
||||
- Identify added, modified, and missing records
|
||||
- Export comparison results for analysis
|
||||
|
||||
### 2. Data Migration
|
||||
|
||||
- Migrate contacts from Dexie to SQLite
|
||||
- Migrate settings from Dexie to SQLite
|
||||
- Option to overwrite existing records or skip them
|
||||
- Comprehensive error handling and reporting
|
||||
|
||||
### 3. User Interface
|
||||
|
||||
- Modern, responsive UI built with Tailwind CSS
|
||||
- Real-time loading states and progress indicators
|
||||
- Clear success and error messaging
|
||||
- Export functionality for comparison data
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Enable Dexie Database
|
||||
|
||||
Before using the migration features, you must enable the Dexie database by setting:
|
||||
|
||||
```typescript
|
||||
// In constants/app.ts
|
||||
export const USE_DEXIE_DB = true;
|
||||
```
|
||||
|
||||
**Note**: This should only be enabled temporarily during migration. Remember to set it back to `false` after migration is complete.
|
||||
|
||||
## Accessing the Migration Interface
|
||||
|
||||
1. Navigate to the **Account** page in the TimeSafari app
|
||||
2. Scroll down to find the **Database Migration** link
|
||||
3. Click the link to open the migration interface
|
||||
|
||||
## Using the Migration Interface
|
||||
|
||||
### Step 1: Compare Databases
|
||||
|
||||
1. Click the **"Compare Databases"** button
|
||||
2. The system will retrieve data from both Dexie and SQLite databases
|
||||
3. Review the comparison results showing:
|
||||
- Summary counts for each database
|
||||
- Detailed differences (added, modified, missing records)
|
||||
- Specific records that need attention
|
||||
|
||||
### Step 2: Review Differences
|
||||
|
||||
The comparison results are displayed in several sections:
|
||||
|
||||
#### Summary Cards
|
||||
|
||||
- **Dexie Contacts**: Number of contacts in Dexie database
|
||||
- **SQLite Contacts**: Number of contacts in SQLite database
|
||||
- **Dexie Settings**: Number of settings in Dexie database
|
||||
- **SQLite Settings**: Number of settings in SQLite database
|
||||
|
||||
#### Contact Differences
|
||||
|
||||
- **Added**: Contacts in Dexie but not in SQLite
|
||||
- **Modified**: Contacts that differ between databases
|
||||
- **Missing**: Contacts in SQLite but not in Dexie
|
||||
|
||||
#### Settings Differences
|
||||
|
||||
- **Added**: Settings in Dexie but not in SQLite
|
||||
- **Modified**: Settings that differ between databases
|
||||
- **Missing**: Settings in SQLite but not in Dexie
|
||||
|
||||
### Step 3: Configure Migration Options
|
||||
|
||||
Before migrating data, configure the migration options:
|
||||
|
||||
- **Overwrite existing records**: When enabled, existing records in SQLite will be updated with data from Dexie. When disabled, existing records will be skipped.
|
||||
|
||||
### Step 4: Migrate Data
|
||||
|
||||
#### Migrate Contacts
|
||||
|
||||
1. Click the **"Migrate Contacts"** button
|
||||
2. The system will transfer contacts from Dexie to SQLite
|
||||
3. Review the migration results showing:
|
||||
- Number of contacts successfully migrated
|
||||
- Any warnings or errors encountered
|
||||
|
||||
#### Migrate Settings
|
||||
|
||||
1. Click the **"Migrate Settings"** button
|
||||
2. The system will transfer settings from Dexie to SQLite
|
||||
3. Review the migration results showing:
|
||||
- Number of settings successfully migrated
|
||||
- Any warnings or errors encountered
|
||||
|
||||
### Step 5: Export Comparison (Optional)
|
||||
|
||||
1. Click the **"Export Comparison"** button
|
||||
2. A JSON file will be downloaded containing the complete comparison data
|
||||
3. This file can be used for analysis or backup purposes
|
||||
|
||||
## Migration Process Details
|
||||
|
||||
### Contact Migration
|
||||
|
||||
The contact migration process:
|
||||
|
||||
1. **Retrieves** all contacts from Dexie database
|
||||
2. **Checks** for existing contacts in SQLite by DID
|
||||
3. **Inserts** new contacts or **updates** existing ones (if overwrite is enabled)
|
||||
4. **Handles** complex fields like `contactMethods` (JSON arrays)
|
||||
5. **Reports** success/failure for each contact
|
||||
|
||||
### Settings Migration
|
||||
|
||||
The settings migration process:
|
||||
|
||||
1. **Retrieves** all settings from Dexie database
|
||||
2. **Focuses** on key user-facing settings:
|
||||
- `firstName`
|
||||
- `isRegistered`
|
||||
- `profileImageUrl`
|
||||
- `showShortcutBvc`
|
||||
- `searchBoxes`
|
||||
3. **Preserves** other settings in SQLite
|
||||
4. **Reports** success/failure for each setting
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Dexie Database Not Enabled
|
||||
|
||||
**Error**: "Dexie database is not enabled"
|
||||
**Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts`
|
||||
|
||||
#### Database Connection Issues
|
||||
|
||||
**Error**: "Failed to retrieve Dexie contacts"
|
||||
**Solution**: Check that the Dexie database is properly initialized and accessible
|
||||
|
||||
#### SQLite Query Errors
|
||||
|
||||
**Error**: "Failed to retrieve SQLite contacts"
|
||||
**Solution**: Verify that the SQLite database is properly set up and the platform service is working
|
||||
|
||||
#### Migration Failures
|
||||
|
||||
**Error**: "Migration failed: [specific error]"
|
||||
**Solution**: Review the error details and check data integrity in both databases
|
||||
|
||||
### Error Recovery
|
||||
|
||||
1. **Review** the error messages carefully
|
||||
2. **Check** the browser console for additional details
|
||||
3. **Verify** database connectivity and permissions
|
||||
4. **Retry** the operation if appropriate
|
||||
5. **Export** comparison data for manual review if needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Before Migration
|
||||
|
||||
1. **Backup** your data if possible
|
||||
2. **Test** the migration on a small dataset first
|
||||
3. **Verify** that both databases are accessible
|
||||
4. **Review** the comparison results before migrating
|
||||
|
||||
### During Migration
|
||||
|
||||
1. **Don't** interrupt the migration process
|
||||
2. **Monitor** the progress and error messages
|
||||
3. **Note** any warnings or skipped records
|
||||
4. **Export** comparison data for reference
|
||||
|
||||
### After Migration
|
||||
|
||||
1. **Verify** that data was migrated correctly
|
||||
2. **Test** the application functionality
|
||||
3. **Disable** Dexie database (`USE_DEXIE_DB = false`)
|
||||
4. **Clean up** any temporary files or exports
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Database Schema
|
||||
|
||||
The migration handles the following data structures:
|
||||
|
||||
#### Contacts Table
|
||||
|
||||
```typescript
|
||||
interface Contact {
|
||||
did: string; // Decentralized Identifier
|
||||
name: string; // Contact name
|
||||
contactMethods: ContactMethod[]; // Array of contact methods
|
||||
nextPubKeyHashB64: string; // Next public key hash
|
||||
notes: string; // Contact notes
|
||||
profileImageUrl: string; // Profile image URL
|
||||
publicKeyBase64: string; // Public key in base64
|
||||
seesMe: boolean; // Visibility flag
|
||||
registered: boolean; // Registration status
|
||||
}
|
||||
```
|
||||
|
||||
#### Settings Table
|
||||
|
||||
```typescript
|
||||
interface Settings {
|
||||
id: number; // Settings ID
|
||||
accountDid: string; // Account DID
|
||||
activeDid: string; // Active DID
|
||||
firstName: string; // User's first name
|
||||
isRegistered: boolean; // Registration status
|
||||
profileImageUrl: string; // Profile image URL
|
||||
showShortcutBvc: boolean; // UI preference
|
||||
searchBoxes: any[]; // Search configuration
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
### Migration Logic
|
||||
|
||||
The migration service uses sophisticated comparison logic:
|
||||
|
||||
1. **Primary Key Matching**: Uses DID for contacts, ID for settings
|
||||
2. **Deep Comparison**: Compares all fields including complex objects
|
||||
3. **JSON Handling**: Properly handles JSON fields like `contactMethods` and `searchBoxes`
|
||||
4. **Conflict Resolution**: Provides options for handling existing records
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- **Batch Processing**: Processes records one by one for reliability
|
||||
- **Error Isolation**: Individual record failures don't stop the entire migration
|
||||
- **Memory Management**: Handles large datasets efficiently
|
||||
- **Progress Reporting**: Provides real-time feedback during migration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migration Stuck
|
||||
|
||||
If the migration appears to be stuck:
|
||||
|
||||
1. **Check** the browser console for errors
|
||||
2. **Refresh** the page and try again
|
||||
3. **Verify** database connectivity
|
||||
4. **Check** for large datasets that might take time
|
||||
|
||||
### Incomplete Migration
|
||||
|
||||
If migration doesn't complete:
|
||||
|
||||
1. **Review** error messages
|
||||
2. **Check** data integrity in both databases
|
||||
3. **Export** comparison data for manual review
|
||||
4. **Consider** migrating in smaller batches
|
||||
|
||||
### Data Inconsistencies
|
||||
|
||||
If you notice data inconsistencies:
|
||||
|
||||
1. **Export** comparison data
|
||||
2. **Review** the differences carefully
|
||||
3. **Manually** verify critical records
|
||||
4. **Consider** selective migration of specific records
|
||||
|
||||
## Support
|
||||
|
||||
For issues with the Database Migration feature:
|
||||
|
||||
1. **Check** this documentation first
|
||||
2. **Review** the browser console for error details
|
||||
3. **Export** comparison data for analysis
|
||||
4. **Contact** the development team with specific error details
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Data Privacy**: Migration data is processed locally and not sent to external servers
|
||||
- **Access Control**: Only users with access to the account can perform migration
|
||||
- **Data Integrity**: Migration preserves data integrity and handles conflicts gracefully
|
||||
- **Audit Trail**: Export functionality provides an audit trail of migration operations
|
||||
|
||||
---
|
||||
|
||||
**Note**: This migration tool is designed for the transition period between database systems. Once migration is complete and verified, the Dexie database should be disabled to avoid confusion and potential data conflicts.
|
||||
418
doc/dexie-to-sqlite-mapping.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# Dexie to absurd-sql Mapping Guide
|
||||
|
||||
## Schema Mapping
|
||||
|
||||
### Current Dexie Schema
|
||||
|
||||
```typescript
|
||||
// Current Dexie schema
|
||||
const db = new Dexie('TimeSafariDB');
|
||||
|
||||
db.version(1).stores({
|
||||
accounts: 'did, publicKeyHex, createdAt, updatedAt',
|
||||
settings: 'key, value, updatedAt',
|
||||
contacts: 'id, did, name, createdAt, updatedAt'
|
||||
});
|
||||
```
|
||||
|
||||
### New SQLite Schema
|
||||
|
||||
```sql
|
||||
-- New SQLite schema
|
||||
CREATE TABLE accounts (
|
||||
did TEXT PRIMARY KEY,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE contacts (
|
||||
id TEXT PRIMARY KEY,
|
||||
did TEXT NOT NULL,
|
||||
name TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (did) REFERENCES accounts(did)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
||||
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||
```
|
||||
|
||||
## Query Mapping
|
||||
|
||||
### 1. Account Operations
|
||||
|
||||
#### Get Account by DID
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
const account = await db.accounts.get(did);
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM accounts WHERE did = ?
|
||||
`, [did]);
|
||||
const account = result[0]?.values[0];
|
||||
```
|
||||
|
||||
#### Get All Accounts
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
const accounts = await db.accounts.toArray();
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM accounts ORDER BY created_at DESC
|
||||
`);
|
||||
const accounts = result[0]?.values || [];
|
||||
```
|
||||
|
||||
#### Add Account
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.accounts.add({
|
||||
did,
|
||||
publicKeyHex,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [did, publicKeyHex, Date.now(), Date.now()]);
|
||||
```
|
||||
|
||||
#### Update Account
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.accounts.update(did, {
|
||||
publicKeyHex,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
UPDATE accounts
|
||||
SET public_key_hex = ?, updated_at = ?
|
||||
WHERE did = ?
|
||||
`, [publicKeyHex, Date.now(), did]);
|
||||
```
|
||||
|
||||
### 2. Settings Operations
|
||||
|
||||
#### Get Setting
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
const setting = await db.settings.get(key);
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM settings WHERE key = ?
|
||||
`, [key]);
|
||||
const setting = result[0]?.values[0];
|
||||
```
|
||||
|
||||
#### Set Setting
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.settings.put({
|
||||
key,
|
||||
value,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
INSERT INTO settings (key, value, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = excluded.updated_at
|
||||
`, [key, value, Date.now()]);
|
||||
```
|
||||
|
||||
### 3. Contact Operations
|
||||
|
||||
#### Get Contacts by Account
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
const contacts = await db.contacts
|
||||
.where('did')
|
||||
.equals(accountDid)
|
||||
.toArray();
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM contacts
|
||||
WHERE did = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [accountDid]);
|
||||
const contacts = result[0]?.values || [];
|
||||
```
|
||||
|
||||
#### Add Contact
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.contacts.add({
|
||||
id: generateId(),
|
||||
did: accountDid,
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [generateId(), accountDid, name, Date.now(), Date.now()]);
|
||||
```
|
||||
|
||||
## Transaction Mapping
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.transaction('rw', [db.accounts, db.contacts], async () => {
|
||||
await db.accounts.add(account);
|
||||
await db.contacts.bulkAdd(contacts);
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.exec('BEGIN TRANSACTION;');
|
||||
try {
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||
|
||||
for (const contact of contacts) {
|
||||
await db.run(`
|
||||
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]);
|
||||
}
|
||||
await db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Helper Functions
|
||||
|
||||
### 1. Data Export (Dexie to JSON)
|
||||
|
||||
```typescript
|
||||
async function exportDexieData(): Promise<MigrationData> {
|
||||
const db = new Dexie('TimeSafariDB');
|
||||
|
||||
return {
|
||||
accounts: await db.accounts.toArray(),
|
||||
settings: await db.settings.toArray(),
|
||||
contacts: await db.contacts.toArray(),
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
timestamp: Date.now(),
|
||||
dexieVersion: Dexie.version
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Import (JSON to absurd-sql)
|
||||
|
||||
```typescript
|
||||
async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
||||
await db.exec('BEGIN TRANSACTION;');
|
||||
try {
|
||||
// Import accounts
|
||||
for (const account of data.accounts) {
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||
}
|
||||
|
||||
// Import settings
|
||||
for (const setting of data.settings) {
|
||||
await db.run(`
|
||||
INSERT INTO settings (key, value, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
`, [setting.key, setting.value, setting.updatedAt]);
|
||||
}
|
||||
|
||||
// Import contacts
|
||||
for (const contact of data.contacts) {
|
||||
await db.run(`
|
||||
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]);
|
||||
}
|
||||
await db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Verification
|
||||
|
||||
```typescript
|
||||
async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
||||
// Verify account count
|
||||
const accountResult = await db.exec('SELECT COUNT(*) as count FROM accounts');
|
||||
const accountCount = accountResult[0].values[0][0];
|
||||
if (accountCount !== dexieData.accounts.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify settings count
|
||||
const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings');
|
||||
const settingsCount = settingsResult[0].values[0][0];
|
||||
if (settingsCount !== dexieData.settings.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify contacts count
|
||||
const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts');
|
||||
const contactsCount = contactsResult[0].values[0][0];
|
||||
if (contactsCount !== dexieData.contacts.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify data integrity
|
||||
for (const account of dexieData.accounts) {
|
||||
const result = await db.exec(
|
||||
'SELECT * FROM accounts WHERE did = ?',
|
||||
[account.did]
|
||||
);
|
||||
const migratedAccount = result[0]?.values[0];
|
||||
if (!migratedAccount ||
|
||||
migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Indexing
|
||||
|
||||
- Dexie automatically creates indexes based on the schema
|
||||
- absurd-sql requires explicit index creation
|
||||
- Added indexes for frequently queried fields
|
||||
- Use `PRAGMA journal_mode=MEMORY;` for better performance
|
||||
|
||||
### 2. Batch Operations
|
||||
|
||||
- Dexie has built-in bulk operations
|
||||
- absurd-sql uses transactions for batch operations
|
||||
- Consider chunking large datasets
|
||||
- Use prepared statements for repeated queries
|
||||
|
||||
### 3. Query Optimization
|
||||
|
||||
- Dexie uses IndexedDB's native indexing
|
||||
- absurd-sql requires explicit query optimization
|
||||
- Use prepared statements for repeated queries
|
||||
- Consider using `PRAGMA synchronous=NORMAL;` for better performance
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 1. Common Errors
|
||||
|
||||
```typescript
|
||||
// Dexie errors
|
||||
try {
|
||||
await db.accounts.add(account);
|
||||
} catch (error) {
|
||||
if (error instanceof Dexie.ConstraintError) {
|
||||
// Handle duplicate key
|
||||
}
|
||||
}
|
||||
|
||||
// absurd-sql errors
|
||||
try {
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||
} catch (error) {
|
||||
if (error.message.includes('UNIQUE constraint failed')) {
|
||||
// Handle duplicate key
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Transaction Recovery
|
||||
|
||||
```typescript
|
||||
// Dexie transaction
|
||||
try {
|
||||
await db.transaction('rw', db.accounts, async () => {
|
||||
// Operations
|
||||
});
|
||||
} catch (error) {
|
||||
// Dexie automatically rolls back
|
||||
}
|
||||
|
||||
// absurd-sql transaction
|
||||
try {
|
||||
await db.exec('BEGIN TRANSACTION;');
|
||||
// Operations
|
||||
await db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. **Preparation**
|
||||
- Export all Dexie data
|
||||
- Verify data integrity
|
||||
- Create SQLite schema
|
||||
- Setup indexes
|
||||
|
||||
2. **Migration**
|
||||
- Import data in transactions
|
||||
- Verify each batch
|
||||
- Handle errors gracefully
|
||||
- Maintain backup
|
||||
|
||||
3. **Verification**
|
||||
- Compare record counts
|
||||
- Verify data integrity
|
||||
- Test common queries
|
||||
- Validate relationships
|
||||
|
||||
4. **Cleanup**
|
||||
- Remove Dexie database
|
||||
- Clear IndexedDB storage
|
||||
- Update application code
|
||||
- Remove old dependencies
|
||||
272
doc/migration-fence-definition.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Migration Fence Definition: Dexie to SQLite
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the **migration fence** - the boundary between the legacy Dexie (IndexedDB) storage system and the new SQLite-based storage system in TimeSafari. The fence ensures controlled migration while maintaining data integrity and application stability.
|
||||
|
||||
## Current Migration Status
|
||||
|
||||
### ✅ Completed Components
|
||||
- **SQLite Database Service**: Fully implemented with absurd-sql
|
||||
- **Platform Service Layer**: Unified database interface across platforms
|
||||
- **Migration Tools**: Data comparison and transfer utilities
|
||||
- **Schema Migration**: Complete table structure migration
|
||||
- **Data Export/Import**: Backup and restore functionality
|
||||
|
||||
### 🔄 Active Migration Components
|
||||
- **Settings Migration**: Core user settings transferred
|
||||
- **Account Migration**: Identity and key management
|
||||
- **Contact Migration**: User contact data (via import interface)
|
||||
|
||||
### ❌ Legacy Components (Fence Boundary)
|
||||
- **Dexie Database**: Legacy IndexedDB storage (disabled by default)
|
||||
- **Dexie-Specific Code**: Direct database access patterns
|
||||
- **Legacy Migration Paths**: Old data transfer methods
|
||||
|
||||
## Migration Fence Definition
|
||||
|
||||
### 1. Configuration Boundary
|
||||
|
||||
```typescript
|
||||
// src/constants/app.ts
|
||||
export const USE_DEXIE_DB = false; // FENCE: Controls legacy database access
|
||||
```
|
||||
|
||||
**Fence Rule**: When `USE_DEXIE_DB = false`:
|
||||
- All new data operations use SQLite
|
||||
- Legacy Dexie database is not initialized
|
||||
- Migration tools are the only path to legacy data
|
||||
|
||||
**Fence Rule**: When `USE_DEXIE_DB = true`:
|
||||
- Legacy database is available for migration
|
||||
- Dual-write operations may be enabled
|
||||
- Migration tools can access both databases
|
||||
|
||||
### 2. Service Layer Boundary
|
||||
|
||||
```typescript
|
||||
// src/services/PlatformServiceFactory.ts
|
||||
export class PlatformServiceFactory {
|
||||
public static getInstance(): PlatformService {
|
||||
// FENCE: All database operations go through platform service
|
||||
// No direct Dexie access outside migration tools
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fence Rule**: All database operations must use:
|
||||
- `PlatformService.dbQuery()` for read operations
|
||||
- `PlatformService.dbExec()` for write operations
|
||||
- No direct `db.` or `accountsDBPromise` access in application code
|
||||
|
||||
### 3. Data Access Patterns
|
||||
|
||||
#### ✅ Allowed (Inside Fence)
|
||||
```typescript
|
||||
// Use platform service for all database operations
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const contacts = await platformService.dbQuery(
|
||||
"SELECT * FROM contacts WHERE did = ?",
|
||||
[accountDid]
|
||||
);
|
||||
```
|
||||
|
||||
#### ❌ Forbidden (Outside Fence)
|
||||
```typescript
|
||||
// Direct Dexie access (legacy pattern)
|
||||
const contacts = await db.contacts.where('did').equals(accountDid).toArray();
|
||||
|
||||
// Direct database reference
|
||||
const result = await accountsDBPromise;
|
||||
```
|
||||
|
||||
### 4. Migration Tool Boundary
|
||||
|
||||
```typescript
|
||||
// src/services/indexedDBMigrationService.ts
|
||||
// FENCE: Only migration tools can access both databases
|
||||
export async function compareDatabases(): Promise<DataComparison> {
|
||||
// This is the ONLY place where both databases are accessed
|
||||
}
|
||||
```
|
||||
|
||||
**Fence Rule**: Migration tools are the exclusive interface between:
|
||||
- Legacy Dexie database
|
||||
- New SQLite database
|
||||
- Data comparison and transfer operations
|
||||
|
||||
## Migration Fence Guidelines
|
||||
|
||||
### 1. Code Development Rules
|
||||
|
||||
#### New Feature Development
|
||||
- **Always** use `PlatformService` for database operations
|
||||
- **Never** import or reference Dexie directly
|
||||
- **Always** test with `USE_DEXIE_DB = false`
|
||||
|
||||
#### Legacy Code Maintenance
|
||||
- **Only** modify Dexie code for migration purposes
|
||||
- **Always** add migration tests for schema changes
|
||||
- **Never** add new Dexie-specific features
|
||||
|
||||
### 2. Data Integrity Rules
|
||||
|
||||
#### Migration Safety
|
||||
- **Always** create backups before migration
|
||||
- **Always** verify data integrity after migration
|
||||
- **Never** delete legacy data until verified
|
||||
|
||||
#### Rollback Strategy
|
||||
- **Always** maintain ability to rollback to Dexie
|
||||
- **Always** preserve migration logs
|
||||
- **Never** assume migration is irreversible
|
||||
|
||||
### 3. Testing Requirements
|
||||
|
||||
#### Migration Testing
|
||||
```typescript
|
||||
// Required test pattern for migration
|
||||
describe('Database Migration', () => {
|
||||
it('should migrate data without loss', async () => {
|
||||
// 1. Enable Dexie
|
||||
// 2. Create test data
|
||||
// 3. Run migration
|
||||
// 4. Verify data integrity
|
||||
// 5. Disable Dexie
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Application Testing
|
||||
```typescript
|
||||
// Required test pattern for application features
|
||||
describe('Feature with Database', () => {
|
||||
it('should work with SQLite only', async () => {
|
||||
// Test with USE_DEXIE_DB = false
|
||||
// Verify all operations use PlatformService
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Migration Fence Enforcement
|
||||
|
||||
### 1. Static Analysis
|
||||
|
||||
#### ESLint Rules
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["../db/index"],
|
||||
"message": "Use PlatformService instead of direct Dexie access"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### TypeScript Rules
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Runtime Checks
|
||||
|
||||
#### Development Mode Validation
|
||||
```typescript
|
||||
// Development-only fence validation
|
||||
if (import.meta.env.DEV && USE_DEXIE_DB) {
|
||||
console.warn('⚠️ Dexie is enabled - migration mode active');
|
||||
}
|
||||
```
|
||||
|
||||
#### Production Safety
|
||||
```typescript
|
||||
// Production fence enforcement
|
||||
if (import.meta.env.PROD && USE_DEXIE_DB) {
|
||||
throw new Error('Dexie cannot be enabled in production');
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Fence Timeline
|
||||
|
||||
### Phase 1: Fence Establishment ✅
|
||||
- [x] Define migration fence boundaries
|
||||
- [x] Implement PlatformService layer
|
||||
- [x] Create migration tools
|
||||
- [x] Set `USE_DEXIE_DB = false` by default
|
||||
|
||||
### Phase 2: Data Migration 🔄
|
||||
- [x] Migrate core settings
|
||||
- [x] Migrate account data
|
||||
- [ ] Complete contact migration
|
||||
- [ ] Verify all data integrity
|
||||
|
||||
### Phase 3: Code Cleanup 📋
|
||||
- [ ] Remove unused Dexie imports
|
||||
- [ ] Clean up legacy database code
|
||||
- [ ] Update all documentation
|
||||
- [ ] Remove migration tools
|
||||
|
||||
### Phase 4: Fence Removal 🎯
|
||||
- [ ] Remove `USE_DEXIE_DB` constant
|
||||
- [ ] Remove Dexie dependencies
|
||||
- [ ] Remove migration service
|
||||
- [ ] Finalize SQLite-only architecture
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. Data Protection
|
||||
- **Encryption**: Maintain encryption standards across migration
|
||||
- **Access Control**: Preserve user privacy during migration
|
||||
- **Audit Trail**: Log all migration operations
|
||||
|
||||
### 2. Error Handling
|
||||
- **Graceful Degradation**: Handle migration failures gracefully
|
||||
- **User Communication**: Clear messaging about migration status
|
||||
- **Recovery Options**: Provide rollback mechanisms
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Migration Performance
|
||||
- **Batch Operations**: Use transactions for bulk data transfer
|
||||
- **Progress Indicators**: Show migration progress to users
|
||||
- **Background Processing**: Non-blocking migration operations
|
||||
|
||||
### 2. Application Performance
|
||||
- **Query Optimization**: Optimize SQLite queries for performance
|
||||
- **Indexing Strategy**: Maintain proper database indexes
|
||||
- **Memory Management**: Efficient memory usage during migration
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
### 1. Code Documentation
|
||||
- **Migration Fence Comments**: Document fence boundaries in code
|
||||
- **API Documentation**: Update all database API documentation
|
||||
- **Migration Guides**: Comprehensive migration documentation
|
||||
|
||||
### 2. User Documentation
|
||||
- **Migration Instructions**: Clear user migration steps
|
||||
- **Troubleshooting**: Common migration issues and solutions
|
||||
- **Rollback Instructions**: How to revert if needed
|
||||
|
||||
## Conclusion
|
||||
|
||||
The migration fence provides a controlled boundary between legacy and new database systems, ensuring:
|
||||
- **Data Integrity**: No data loss during migration
|
||||
- **Application Stability**: Consistent behavior across platforms
|
||||
- **Development Clarity**: Clear guidelines for code development
|
||||
- **Migration Safety**: Controlled and reversible migration process
|
||||
|
||||
This fence will remain in place until all data is successfully migrated and verified, at which point the legacy system can be safely removed.
|
||||
355
doc/migration-security-checklist.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Database Migration Security Audit Checklist
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a comprehensive security audit checklist for the Dexie to SQLite migration in TimeSafari. The checklist ensures that data protection, privacy, and security are maintained throughout the migration process.
|
||||
|
||||
## Pre-Migration Security Assessment
|
||||
|
||||
### 1. Data Classification and Sensitivity
|
||||
|
||||
- [ ] **Data Inventory**
|
||||
- [ ] Identify all sensitive data types (DIDs, private keys, personal information)
|
||||
- [ ] Document data retention requirements
|
||||
- [ ] Map data relationships and dependencies
|
||||
- [ ] Assess data sensitivity levels (public, internal, confidential, restricted)
|
||||
|
||||
- [ ] **Encryption Assessment**
|
||||
- [ ] Verify current encryption methods for sensitive data
|
||||
- [ ] Document encryption keys and their management
|
||||
- [ ] Assess encryption strength and compliance
|
||||
- [ ] Plan encryption migration strategy
|
||||
|
||||
### 2. Access Control Review
|
||||
|
||||
- [ ] **User Access Rights**
|
||||
- [ ] Audit current user permissions and roles
|
||||
- [ ] Document access control mechanisms
|
||||
- [ ] Verify principle of least privilege
|
||||
- [ ] Plan access control migration
|
||||
|
||||
- [ ] **System Access**
|
||||
- [ ] Review database access patterns
|
||||
- [ ] Document authentication mechanisms
|
||||
- [ ] Assess session management
|
||||
- [ ] Plan authentication migration
|
||||
|
||||
### 3. Compliance Requirements
|
||||
|
||||
- [ ] **Regulatory Compliance**
|
||||
- [ ] Identify applicable regulations (GDPR, CCPA, etc.)
|
||||
- [ ] Document data processing requirements
|
||||
- [ ] Assess privacy impact
|
||||
- [ ] Plan compliance verification
|
||||
|
||||
- [ ] **Industry Standards**
|
||||
- [ ] Review security standards compliance
|
||||
- [ ] Document security controls
|
||||
- [ ] Assess audit requirements
|
||||
- [ ] Plan standards compliance
|
||||
|
||||
## Migration Security Controls
|
||||
|
||||
### 1. Data Protection During Migration
|
||||
|
||||
- [ ] **Encryption in Transit**
|
||||
- [ ] Verify all data transfers are encrypted
|
||||
- [ ] Use secure communication protocols (TLS 1.3+)
|
||||
- [ ] Implement secure API endpoints
|
||||
- [ ] Monitor encryption status
|
||||
|
||||
- [ ] **Encryption at Rest**
|
||||
- [ ] Maintain encryption for stored data
|
||||
- [ ] Verify encryption key management
|
||||
- [ ] Test encryption/decryption processes
|
||||
- [ ] Document encryption procedures
|
||||
|
||||
### 2. Access Control During Migration
|
||||
|
||||
- [ ] **Authentication**
|
||||
- [ ] Maintain user authentication during migration
|
||||
- [ ] Verify session management
|
||||
- [ ] Implement secure token handling
|
||||
- [ ] Monitor authentication events
|
||||
|
||||
- [ ] **Authorization**
|
||||
- [ ] Preserve user permissions during migration
|
||||
- [ ] Verify role-based access control
|
||||
- [ ] Implement audit logging
|
||||
- [ ] Monitor access patterns
|
||||
|
||||
### 3. Data Integrity
|
||||
|
||||
- [ ] **Data Validation**
|
||||
- [ ] Implement input validation for all data
|
||||
- [ ] Verify data format consistency
|
||||
- [ ] Test data transformation processes
|
||||
- [ ] Document validation rules
|
||||
|
||||
- [ ] **Data Verification**
|
||||
- [ ] Implement checksums for data integrity
|
||||
- [ ] Verify data completeness after migration
|
||||
- [ ] Test data consistency checks
|
||||
- [ ] Document verification procedures
|
||||
|
||||
## Migration Process Security
|
||||
|
||||
### 1. Backup Security
|
||||
|
||||
- [ ] **Backup Creation**
|
||||
- [ ] Create encrypted backups before migration
|
||||
- [ ] Verify backup integrity
|
||||
- [ ] Store backups securely
|
||||
- [ ] Test backup restoration
|
||||
|
||||
- [ ] **Backup Access**
|
||||
- [ ] Limit backup access to authorized personnel
|
||||
- [ ] Implement backup access logging
|
||||
- [ ] Verify backup encryption
|
||||
- [ ] Document backup procedures
|
||||
|
||||
### 2. Migration Tool Security
|
||||
|
||||
- [ ] **Tool Authentication**
|
||||
- [ ] Implement secure authentication for migration tools
|
||||
- [ ] Verify tool access controls
|
||||
- [ ] Monitor tool usage
|
||||
- [ ] Document tool security
|
||||
|
||||
- [ ] **Tool Validation**
|
||||
- [ ] Verify migration tool integrity
|
||||
- [ ] Test tool security features
|
||||
- [ ] Validate tool outputs
|
||||
- [ ] Document tool validation
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] **Error Security**
|
||||
- [ ] Implement secure error handling
|
||||
- [ ] Avoid information disclosure in errors
|
||||
- [ ] Log security-relevant errors
|
||||
- [ ] Document error procedures
|
||||
|
||||
- [ ] **Recovery Security**
|
||||
- [ ] Implement secure recovery procedures
|
||||
- [ ] Verify recovery data protection
|
||||
- [ ] Test recovery processes
|
||||
- [ ] Document recovery security
|
||||
|
||||
## Post-Migration Security
|
||||
|
||||
### 1. Data Verification
|
||||
|
||||
- [ ] **Data Completeness**
|
||||
- [ ] Verify all data was migrated successfully
|
||||
- [ ] Check for data corruption
|
||||
- [ ] Validate data relationships
|
||||
- [ ] Document verification results
|
||||
|
||||
- [ ] **Data Accuracy**
|
||||
- [ ] Verify data accuracy after migration
|
||||
- [ ] Test data consistency
|
||||
- [ ] Validate data integrity
|
||||
- [ ] Document accuracy checks
|
||||
|
||||
### 2. Access Control Verification
|
||||
|
||||
- [ ] **User Access**
|
||||
- [ ] Verify user access rights after migration
|
||||
- [ ] Test authentication mechanisms
|
||||
- [ ] Validate authorization rules
|
||||
- [ ] Document access verification
|
||||
|
||||
- [ ] **System Access**
|
||||
- [ ] Verify system access controls
|
||||
- [ ] Test API security
|
||||
- [ ] Validate session management
|
||||
- [ ] Document system security
|
||||
|
||||
### 3. Security Testing
|
||||
|
||||
- [ ] **Penetration Testing**
|
||||
- [ ] Conduct security penetration testing
|
||||
- [ ] Test for common vulnerabilities
|
||||
- [ ] Verify security controls
|
||||
- [ ] Document test results
|
||||
|
||||
- [ ] **Vulnerability Assessment**
|
||||
- [ ] Scan for security vulnerabilities
|
||||
- [ ] Assess security posture
|
||||
- [ ] Identify security gaps
|
||||
- [ ] Document assessment results
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### 1. Security Monitoring
|
||||
|
||||
- [ ] **Access Monitoring**
|
||||
- [ ] Monitor database access patterns
|
||||
- [ ] Track user authentication events
|
||||
- [ ] Monitor system access
|
||||
- [ ] Document monitoring procedures
|
||||
|
||||
- [ ] **Data Monitoring**
|
||||
- [ ] Monitor data access patterns
|
||||
- [ ] Track data modification events
|
||||
- [ ] Monitor data integrity
|
||||
- [ ] Document data monitoring
|
||||
|
||||
### 2. Security Logging
|
||||
|
||||
- [ ] **Audit Logging**
|
||||
- [ ] Implement comprehensive audit logging
|
||||
- [ ] Log all security-relevant events
|
||||
- [ ] Secure log storage and access
|
||||
- [ ] Document logging procedures
|
||||
|
||||
- [ ] **Log Analysis**
|
||||
- [ ] Implement log analysis tools
|
||||
- [ ] Monitor for security incidents
|
||||
- [ ] Analyze security trends
|
||||
- [ ] Document analysis procedures
|
||||
|
||||
## Incident Response
|
||||
|
||||
### 1. Security Incident Planning
|
||||
|
||||
- [ ] **Incident Response Plan**
|
||||
- [ ] Develop security incident response plan
|
||||
- [ ] Define incident response procedures
|
||||
- [ ] Train incident response team
|
||||
- [ ] Document response procedures
|
||||
|
||||
- [ ] **Incident Detection**
|
||||
- [ ] Implement incident detection mechanisms
|
||||
- [ ] Monitor for security incidents
|
||||
- [ ] Establish incident reporting procedures
|
||||
- [ ] Document detection procedures
|
||||
|
||||
### 2. Recovery Procedures
|
||||
|
||||
- [ ] **Data Recovery**
|
||||
- [ ] Develop data recovery procedures
|
||||
- [ ] Test recovery processes
|
||||
- [ ] Verify recovery data integrity
|
||||
- [ ] Document recovery procedures
|
||||
|
||||
- [ ] **System Recovery**
|
||||
- [ ] Develop system recovery procedures
|
||||
- [ ] Test system recovery
|
||||
- [ ] Verify system security after recovery
|
||||
- [ ] Document recovery procedures
|
||||
|
||||
## Compliance Verification
|
||||
|
||||
### 1. Regulatory Compliance
|
||||
|
||||
- [ ] **Privacy Compliance**
|
||||
- [ ] Verify GDPR compliance
|
||||
- [ ] Check CCPA compliance
|
||||
- [ ] Assess other privacy regulations
|
||||
- [ ] Document compliance status
|
||||
|
||||
- [ ] **Security Compliance**
|
||||
- [ ] Verify security standard compliance
|
||||
- [ ] Check industry requirements
|
||||
- [ ] Assess security certifications
|
||||
- [ ] Document compliance status
|
||||
|
||||
### 2. Audit Requirements
|
||||
|
||||
- [ ] **Audit Trail**
|
||||
- [ ] Maintain comprehensive audit trail
|
||||
- [ ] Verify audit log integrity
|
||||
- [ ] Test audit log accessibility
|
||||
- [ ] Document audit procedures
|
||||
|
||||
- [ ] **Audit Reporting**
|
||||
- [ ] Generate audit reports
|
||||
- [ ] Verify report accuracy
|
||||
- [ ] Distribute reports securely
|
||||
- [ ] Document reporting procedures
|
||||
|
||||
## Documentation and Training
|
||||
|
||||
### 1. Security Documentation
|
||||
|
||||
- [ ] **Security Procedures**
|
||||
- [ ] Document security procedures
|
||||
- [ ] Update security policies
|
||||
- [ ] Create security guidelines
|
||||
- [ ] Maintain documentation
|
||||
|
||||
- [ ] **Security Training**
|
||||
- [ ] Develop security training materials
|
||||
- [ ] Train staff on security procedures
|
||||
- [ ] Verify training effectiveness
|
||||
- [ ] Document training procedures
|
||||
|
||||
### 2. Ongoing Security
|
||||
|
||||
- [ ] **Security Maintenance**
|
||||
- [ ] Establish security maintenance procedures
|
||||
- [ ] Schedule security updates
|
||||
- [ ] Monitor security trends
|
||||
- [ ] Document maintenance procedures
|
||||
|
||||
- [ ] **Security Review**
|
||||
- [ ] Conduct regular security reviews
|
||||
- [ ] Update security controls
|
||||
- [ ] Assess security effectiveness
|
||||
- [ ] Document review procedures
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### 1. Risk Identification
|
||||
|
||||
- [ ] **Security Risks**
|
||||
- [ ] Identify potential security risks
|
||||
- [ ] Assess risk likelihood and impact
|
||||
- [ ] Prioritize security risks
|
||||
- [ ] Document risk assessment
|
||||
|
||||
- [ ] **Mitigation Strategies**
|
||||
- [ ] Develop risk mitigation strategies
|
||||
- [ ] Implement risk controls
|
||||
- [ ] Monitor risk status
|
||||
- [ ] Document mitigation procedures
|
||||
|
||||
### 2. Risk Monitoring
|
||||
|
||||
- [ ] **Risk Tracking**
|
||||
- [ ] Track identified risks
|
||||
- [ ] Monitor risk status
|
||||
- [ ] Update risk assessments
|
||||
- [ ] Document risk tracking
|
||||
|
||||
- [ ] **Risk Reporting**
|
||||
- [ ] Generate risk reports
|
||||
- [ ] Distribute risk information
|
||||
- [ ] Update risk documentation
|
||||
- [ ] Document reporting procedures
|
||||
|
||||
## Conclusion
|
||||
|
||||
This security audit checklist ensures that the database migration maintains the highest standards of data protection, privacy, and security. Regular review and updates of this checklist are essential to maintain security throughout the migration process and beyond.
|
||||
|
||||
### Security Checklist Summary
|
||||
|
||||
- [ ] **Pre-Migration Assessment**: Complete
|
||||
- [ ] **Migration Controls**: Complete
|
||||
- [ ] **Process Security**: Complete
|
||||
- [ ] **Post-Migration Verification**: Complete
|
||||
- [ ] **Monitoring and Logging**: Complete
|
||||
- [ ] **Incident Response**: Complete
|
||||
- [ ] **Compliance Verification**: Complete
|
||||
- [ ] **Documentation and Training**: Complete
|
||||
- [ ] **Risk Assessment**: Complete
|
||||
|
||||
**Overall Security Status**: [ ] Secure [ ] Needs Attention [ ] Critical Issues
|
||||
|
||||
**Next Review Date**: _______________
|
||||
|
||||
**Reviewed By**: _______________
|
||||
|
||||
**Approved By**: _______________
|
||||
226
doc/migration-to-wa-sqlite.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Migration Guide: Dexie to absurd-sql
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the migration process from Dexie.js to absurd-sql for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users.
|
||||
|
||||
**Current Status**: The migration is in **Phase 2** with a well-defined migration fence in place. Core settings and account data have been migrated, with contact migration in progress. **ActiveDid migration has been implemented** to ensure user identity continuity.
|
||||
|
||||
## Migration Goals
|
||||
|
||||
1. **Data Integrity**
|
||||
- Preserve all existing data
|
||||
- Maintain data relationships
|
||||
- Ensure data consistency
|
||||
- **Preserve user's active identity**
|
||||
|
||||
2. **Performance**
|
||||
- Improve query performance
|
||||
- Reduce storage overhead
|
||||
- Optimize for platform-specific capabilities
|
||||
|
||||
3. **User Experience**
|
||||
- Seamless transition with no data loss
|
||||
- Maintain user's active identity and preferences
|
||||
- Preserve application state
|
||||
|
||||
## Migration Architecture
|
||||
|
||||
### Migration Fence
|
||||
The migration fence is defined by the `USE_DEXIE_DB` constant in `src/constants/app.ts`:
|
||||
- `USE_DEXIE_DB = false` (default): Uses SQLite database
|
||||
- `USE_DEXIE_DB = true`: Uses Dexie database (for migration purposes)
|
||||
|
||||
### Migration Order
|
||||
The migration follows a specific order to maintain data integrity:
|
||||
|
||||
1. **Accounts** (foundational - contains DIDs)
|
||||
2. **Settings** (references accountDid, activeDid)
|
||||
3. **ActiveDid** (depends on accounts and settings) ⭐ **NEW**
|
||||
4. **Contacts** (independent, but migrated after accounts for consistency)
|
||||
|
||||
## ActiveDid Migration ⭐ **NEW FEATURE**
|
||||
|
||||
### Problem Solved
|
||||
Previously, the `activeDid` setting was not migrated from Dexie to SQLite, causing users to lose their active identity after migration.
|
||||
|
||||
### Solution Implemented
|
||||
The migration now includes a dedicated step for migrating the `activeDid`:
|
||||
|
||||
1. **Detection**: Identifies the `activeDid` from Dexie master settings
|
||||
2. **Validation**: Verifies the `activeDid` exists in SQLite accounts
|
||||
3. **Migration**: Updates SQLite master settings with the `activeDid`
|
||||
4. **Error Handling**: Graceful handling of missing accounts
|
||||
|
||||
### Implementation Details
|
||||
|
||||
#### New Function: `migrateActiveDid()`
|
||||
```typescript
|
||||
export async function migrateActiveDid(): Promise<MigrationResult> {
|
||||
// 1. Get Dexie settings to find the activeDid
|
||||
const dexieSettings = await getDexieSettings();
|
||||
const masterSettings = dexieSettings.find(setting => !setting.accountDid);
|
||||
|
||||
// 2. Verify the activeDid exists in SQLite accounts
|
||||
const accountExists = await platformService.dbQuery(
|
||||
"SELECT did FROM accounts WHERE did = ?",
|
||||
[dexieActiveDid],
|
||||
);
|
||||
|
||||
// 3. Update SQLite master settings
|
||||
await updateDefaultSettings({ activeDid: dexieActiveDid });
|
||||
}
|
||||
```
|
||||
|
||||
#### Enhanced `migrateSettings()` Function
|
||||
The settings migration now includes activeDid handling:
|
||||
- Extracts `activeDid` from Dexie master settings
|
||||
- Validates account existence in SQLite
|
||||
- Updates SQLite master settings with the `activeDid`
|
||||
|
||||
#### Updated `migrateAll()` Function
|
||||
The complete migration now includes a dedicated step for activeDid:
|
||||
```typescript
|
||||
// Step 3: Migrate ActiveDid (depends on accounts and settings)
|
||||
logger.info("[MigrationService] Step 3: Migrating activeDid...");
|
||||
const activeDidResult = await migrateActiveDid();
|
||||
```
|
||||
|
||||
### Benefits
|
||||
- ✅ **User Identity Preservation**: Users maintain their active identity
|
||||
- ✅ **Seamless Experience**: No need to manually select identity after migration
|
||||
- ✅ **Data Consistency**: Ensures all identity-related settings are preserved
|
||||
- ✅ **Error Resilience**: Graceful handling of edge cases
|
||||
|
||||
## Migration Process
|
||||
|
||||
### Phase 1: Preparation ✅
|
||||
- [x] Enable Dexie database access
|
||||
- [x] Implement data comparison tools
|
||||
- [x] Create migration service structure
|
||||
|
||||
### Phase 2: Core Migration ✅
|
||||
- [x] Account migration with `importFromMnemonic`
|
||||
- [x] Settings migration (excluding activeDid)
|
||||
- [x] **ActiveDid migration** ⭐ **COMPLETED**
|
||||
- [x] Contact migration framework
|
||||
|
||||
### Phase 3: Validation and Cleanup 🔄
|
||||
- [ ] Comprehensive data validation
|
||||
- [ ] Performance testing
|
||||
- [ ] User acceptance testing
|
||||
- [ ] Dexie removal
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Migration
|
||||
```typescript
|
||||
import { migrateAll, migrateActiveDid } from '../services/indexedDBMigrationService';
|
||||
|
||||
// Complete migration
|
||||
const result = await migrateAll();
|
||||
|
||||
// Or migrate just the activeDid
|
||||
const activeDidResult = await migrateActiveDid();
|
||||
```
|
||||
|
||||
### Migration Verification
|
||||
```typescript
|
||||
import { compareDatabases } from '../services/indexedDBMigrationService';
|
||||
|
||||
const comparison = await compareDatabases();
|
||||
console.log('Migration differences:', comparison.differences);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### ActiveDid Migration Errors
|
||||
- **Missing Account**: If the `activeDid` from Dexie doesn't exist in SQLite accounts
|
||||
- **Database Errors**: Connection or query failures
|
||||
- **Settings Update Failures**: Issues updating SQLite master settings
|
||||
|
||||
### Recovery Strategies
|
||||
1. **Automatic Recovery**: Migration continues even if activeDid migration fails
|
||||
2. **Manual Recovery**: Users can manually select their identity after migration
|
||||
3. **Fallback**: System creates new identity if none exists
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Data Protection
|
||||
- All sensitive data (mnemonics, private keys) are encrypted
|
||||
- Migration preserves encryption standards
|
||||
- No plaintext data exposure during migration
|
||||
|
||||
### Identity Verification
|
||||
- ActiveDid migration validates account existence
|
||||
- Prevents setting non-existent identities as active
|
||||
- Maintains cryptographic integrity
|
||||
|
||||
## Testing
|
||||
|
||||
### Migration Testing
|
||||
```bash
|
||||
# Enable Dexie for testing
|
||||
# Set USE_DEXIE_DB = true in constants/app.ts
|
||||
|
||||
# Run migration
|
||||
npm run migrate
|
||||
|
||||
# Verify results
|
||||
npm run test:migration
|
||||
```
|
||||
|
||||
### ActiveDid Testing
|
||||
```typescript
|
||||
// Test activeDid migration specifically
|
||||
const result = await migrateActiveDid();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.warnings).toContain('Successfully migrated activeDid');
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **ActiveDid Not Found**
|
||||
- Ensure accounts were migrated before activeDid migration
|
||||
- Check that the Dexie activeDid exists in SQLite accounts
|
||||
|
||||
2. **Migration Failures**
|
||||
- Verify Dexie database is accessible
|
||||
- Check SQLite database permissions
|
||||
- Review migration logs for specific errors
|
||||
|
||||
3. **Data Inconsistencies**
|
||||
- Use `compareDatabases()` to identify differences
|
||||
- Re-run migration if necessary
|
||||
- Check for duplicate or conflicting records
|
||||
|
||||
### Debugging
|
||||
```typescript
|
||||
// Enable detailed logging
|
||||
logger.setLevel('debug');
|
||||
|
||||
// Check migration status
|
||||
const comparison = await compareDatabases();
|
||||
console.log('Settings differences:', comparison.differences.settings);
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Improvements
|
||||
1. **Batch Processing**: Optimize for large datasets
|
||||
2. **Incremental Migration**: Support partial migrations
|
||||
3. **Rollback Capability**: Ability to revert migration
|
||||
4. **Progress Tracking**: Real-time migration progress
|
||||
|
||||
### Performance Optimizations
|
||||
1. **Parallel Processing**: Migrate independent data concurrently
|
||||
2. **Memory Management**: Optimize for large datasets
|
||||
3. **Transaction Batching**: Reduce database round trips
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Dexie to SQLite migration provides a robust, secure, and user-friendly transition path. The addition of activeDid migration ensures that users maintain their identity continuity throughout the migration process, significantly improving the user experience.
|
||||
|
||||
The migration fence architecture allows for controlled, reversible migration while maintaining application stability and data integrity.
|
||||
@@ -1,6 +1,6 @@
|
||||
JWT Creation & Verification
|
||||
|
||||
To run this in a script, see ./openssl_signing_console.sh
|
||||
To run this in a script, see /scripts/openssl_signing_console.sh
|
||||
|
||||
Prerequisites: openssl, jq
|
||||
|
||||
805
doc/qr-code-implementation-guide.md
Normal file
@@ -0,0 +1,805 @@
|
||||
# QR Code Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the QR code scanning and generation implementation in the TimeSafari application. The system uses a platform-agnostic design with specific implementations for web and mobile platforms.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/
|
||||
├── services/
|
||||
│ └── QRScanner/
|
||||
│ ├── types.ts # Core interfaces and types
|
||||
│ ├── QRScannerFactory.ts # Factory for creating scanner instances
|
||||
│ ├── CapacitorQRScanner.ts # Mobile implementation using MLKit
|
||||
│ ├── WebInlineQRScanner.ts # Web implementation using MediaDevices API
|
||||
│ └── interfaces.ts # Additional interfaces
|
||||
├── components/
|
||||
│ └── QRScanner/
|
||||
│ └── QRScannerDialog.vue # Shared UI component
|
||||
└── views/
|
||||
├── ContactQRScanView.vue # Dedicated scanning view
|
||||
└── ContactQRScanShowView.vue # Combined QR display and scanning view
|
||||
```
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Factory Pattern**
|
||||
- `QRScannerFactory` - Creates appropriate scanner instance based on platform
|
||||
- Common interface `QRScannerService` implemented by all scanners
|
||||
- Platform detection via Capacitor and build flags
|
||||
|
||||
2. **Platform-Specific Implementations**
|
||||
- `CapacitorQRScanner` - Native mobile implementation using MLKit
|
||||
- `WebInlineQRScanner` - Web browser implementation using MediaDevices API
|
||||
- `QRScannerDialog.vue` - Shared UI component
|
||||
|
||||
3. **View Components**
|
||||
- `ContactQRScanView` - Dedicated view for scanning QR codes
|
||||
- `ContactQRScanShowView` - Combined view for displaying and scanning QR codes
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Core Interfaces
|
||||
|
||||
```typescript
|
||||
interface QRScannerService {
|
||||
checkPermissions(): Promise<boolean>;
|
||||
requestPermissions(): Promise<boolean>;
|
||||
isSupported(): Promise<boolean>;
|
||||
startScan(options?: QRScannerOptions): Promise<void>;
|
||||
stopScan(): Promise<void>;
|
||||
addListener(listener: ScanListener): void;
|
||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||
cleanup(): Promise<void>;
|
||||
getAvailableCameras(): Promise<MediaDeviceInfo[]>;
|
||||
switchCamera(deviceId: string): Promise<void>;
|
||||
getCurrentCamera(): Promise<MediaDeviceInfo | null>;
|
||||
}
|
||||
|
||||
interface ScanListener {
|
||||
onScan: (result: string) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface QRScannerOptions {
|
||||
camera?: "front" | "back";
|
||||
showPreview?: boolean;
|
||||
playSound?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Platform-Specific Implementations
|
||||
|
||||
#### Mobile (Capacitor)
|
||||
- Uses `@capacitor-mlkit/barcode-scanning`
|
||||
- Native camera access through platform APIs
|
||||
- Optimized for mobile performance
|
||||
- Supports both iOS and Android
|
||||
- Real-time QR code detection
|
||||
- Back camera preferred for scanning
|
||||
|
||||
Configuration:
|
||||
```typescript
|
||||
// capacitor.config.ts
|
||||
const config: CapacitorConfig = {
|
||||
plugins: {
|
||||
MLKitBarcodeScanner: {
|
||||
formats: ['QR_CODE'],
|
||||
detectorSize: 1.0,
|
||||
lensFacing: 'back',
|
||||
googleBarcodeScannerModuleInstallState: true,
|
||||
// Additional camera options
|
||||
cameraOptions: {
|
||||
quality: 0.8,
|
||||
allowEditing: false,
|
||||
resultType: 'uri',
|
||||
sourceType: 'CAMERA',
|
||||
saveToGallery: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Web
|
||||
- Uses browser's MediaDevices API
|
||||
- Vue.js components for UI
|
||||
- EventEmitter for stream management
|
||||
- Browser-based camera access
|
||||
- Inline camera preview
|
||||
- Responsive design
|
||||
- Cross-browser compatibility
|
||||
|
||||
### View Components
|
||||
|
||||
#### ContactQRScanView
|
||||
- Dedicated view for scanning QR codes
|
||||
- Full-screen camera interface
|
||||
- Simple UI focused on scanning
|
||||
- Used primarily on native platforms
|
||||
- Streamlined scanning experience
|
||||
|
||||
#### ContactQRScanShowView
|
||||
- Combined view for QR code display and scanning
|
||||
- Shows user's own QR code
|
||||
- Handles user registration status
|
||||
- Provides options to copy contact information
|
||||
- Platform-specific scanning implementation:
|
||||
- Native: Button to navigate to ContactQRScanView
|
||||
- Web: Built-in scanning functionality
|
||||
|
||||
### QR Code Workflow
|
||||
|
||||
1. **Initiation**
|
||||
- User selects "Scan QR Code" option
|
||||
- Platform-specific scanner is initialized
|
||||
- Camera permissions are verified
|
||||
- Appropriate scanner component is loaded
|
||||
|
||||
2. **Platform-Specific Implementation**
|
||||
- Web: Uses `qrcode-stream` for real-time scanning
|
||||
- Native: Uses `@capacitor-mlkit/barcode-scanning`
|
||||
|
||||
3. **Scanning Process**
|
||||
- Camera stream initialization
|
||||
- Real-time frame analysis
|
||||
- QR code detection and decoding
|
||||
- Validation of QR code format
|
||||
- Processing of contact information
|
||||
|
||||
4. **Contact Processing**
|
||||
- Decryption of contact data
|
||||
- Validation of user information
|
||||
- Verification of timestamp
|
||||
- Check for duplicate contacts
|
||||
- Processing of shared data
|
||||
|
||||
## Build Configuration
|
||||
|
||||
### Common Vite Configuration
|
||||
```typescript
|
||||
// vite.config.common.mts
|
||||
export async function createBuildConfig(mode: string) {
|
||||
const isCapacitor = mode === "capacitor";
|
||||
|
||||
return defineConfig({
|
||||
define: {
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative),
|
||||
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||
__USE_QR_READER__: JSON.stringify(!isCapacitor)
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'@capacitor-mlkit/barcode-scanning',
|
||||
'vue-qrcode-reader'
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Platform-Specific Builds
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build:web": "vite build --config vite.config.web.mts",
|
||||
"build:capacitor": "vite build --config vite.config.capacitor.mts",
|
||||
"build:all": "npm run build:web && npm run build:capacitor"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Scenarios
|
||||
1. No camera found
|
||||
2. Permission denied
|
||||
3. Camera in use by another application
|
||||
4. HTTPS required
|
||||
5. Browser compatibility issues
|
||||
6. Invalid QR code format
|
||||
7. Expired QR codes
|
||||
8. Duplicate contact attempts
|
||||
9. Network connectivity issues
|
||||
|
||||
### Error Response
|
||||
- User-friendly error messages
|
||||
- Troubleshooting tips
|
||||
- Clear instructions for resolution
|
||||
- Platform-specific guidance
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### QR Code Security
|
||||
- Encryption of contact data
|
||||
- Timestamp validation
|
||||
- Version checking
|
||||
- User verification
|
||||
- Rate limiting for scans
|
||||
|
||||
### Data Protection
|
||||
- Secure transmission of contact data
|
||||
- Validation of QR code authenticity
|
||||
- Prevention of duplicate scans
|
||||
- Protection against malicious codes
|
||||
- Secure storage of contact information
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Camera Access
|
||||
1. Always check for camera availability
|
||||
2. Request permissions explicitly
|
||||
3. Handle all error conditions
|
||||
4. Provide clear user feedback
|
||||
5. Implement proper cleanup
|
||||
|
||||
### Performance
|
||||
1. Optimize camera resolution
|
||||
2. Implement proper resource cleanup
|
||||
3. Handle camera switching efficiently
|
||||
4. Manage memory usage
|
||||
5. Battery usage optimization
|
||||
|
||||
### User Experience
|
||||
1. Clear visual feedback
|
||||
2. Camera preview
|
||||
3. Scanning status indicators
|
||||
4. Error messages
|
||||
5. Success confirmations
|
||||
6. Intuitive camera controls
|
||||
7. Smooth camera switching
|
||||
8. Responsive UI feedback
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Scenarios
|
||||
1. Permission handling
|
||||
2. Camera switching
|
||||
3. Error conditions
|
||||
4. Platform compatibility
|
||||
5. Performance metrics
|
||||
6. QR code detection
|
||||
7. Contact processing
|
||||
8. Security validation
|
||||
|
||||
### Test Environment
|
||||
- Multiple browsers
|
||||
- iOS and Android devices
|
||||
- Various network conditions
|
||||
- Different camera configurations
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Key Packages
|
||||
- `@capacitor-mlkit/barcode-scanning`
|
||||
- `qrcode-stream`
|
||||
- `vue-qrcode-reader`
|
||||
- Platform-specific camera APIs
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Updates
|
||||
- Keep dependencies updated
|
||||
- Monitor platform changes
|
||||
- Update documentation
|
||||
- Review security patches
|
||||
|
||||
### Performance Monitoring
|
||||
- Track memory usage
|
||||
- Monitor camera performance
|
||||
- Check error rates
|
||||
- Analyze user feedback
|
||||
|
||||
## Camera Handling
|
||||
|
||||
### Camera Switching Implementation
|
||||
|
||||
The QR scanner supports camera switching on both mobile and desktop platforms through a unified interface.
|
||||
|
||||
#### Platform-Specific Implementations
|
||||
|
||||
1. **Mobile (Capacitor)**
|
||||
- Uses `@capacitor-mlkit/barcode-scanning`
|
||||
- Supports front/back camera switching
|
||||
- Native camera access through platform APIs
|
||||
- Optimized for mobile performance
|
||||
|
||||
```typescript
|
||||
// CapacitorQRScanner.ts
|
||||
async startScan(options?: QRScannerOptions): Promise<void> {
|
||||
const scanOptions: StartScanOptions = {
|
||||
formats: [BarcodeFormat.QrCode],
|
||||
lensFacing: options?.camera === "front" ?
|
||||
LensFacing.Front : LensFacing.Back
|
||||
};
|
||||
await BarcodeScanner.startScan(scanOptions);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Web (Desktop)**
|
||||
- Uses browser's MediaDevices API
|
||||
- Supports multiple camera devices
|
||||
- Dynamic camera enumeration
|
||||
- Real-time camera switching
|
||||
|
||||
```typescript
|
||||
// WebInlineQRScanner.ts
|
||||
async getAvailableCameras(): Promise<MediaDeviceInfo[]> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter(device => device.kind === 'videoinput');
|
||||
}
|
||||
|
||||
async switchCamera(deviceId: string): Promise<void> {
|
||||
// Stop current stream
|
||||
await this.stopScan();
|
||||
|
||||
// Start new stream with selected camera
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
deviceId: { exact: deviceId },
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
|
||||
// Update video and restart scanning
|
||||
if (this.video) {
|
||||
this.video.srcObject = this.stream;
|
||||
await this.video.play();
|
||||
}
|
||||
this.scanQRCode();
|
||||
}
|
||||
```
|
||||
|
||||
### Core Interfaces
|
||||
|
||||
```typescript
|
||||
interface QRScannerService {
|
||||
// ... existing methods ...
|
||||
|
||||
/** Get available cameras */
|
||||
getAvailableCameras(): Promise<MediaDeviceInfo[]>;
|
||||
|
||||
/** Switch to a specific camera */
|
||||
switchCamera(deviceId: string): Promise<void>;
|
||||
|
||||
/** Get current camera info */
|
||||
getCurrentCamera(): Promise<MediaDeviceInfo | null>;
|
||||
}
|
||||
|
||||
interface QRScannerOptions {
|
||||
/** Camera to use ('front' or 'back' for mobile) */
|
||||
camera?: "front" | "back";
|
||||
/** Whether to show a preview of the camera feed */
|
||||
showPreview?: boolean;
|
||||
/** Whether to play a sound on successful scan */
|
||||
playSound?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### UI Components
|
||||
|
||||
The camera switching UI adapts to the platform:
|
||||
|
||||
1. **Mobile Interface**
|
||||
- Simple toggle button for front/back cameras
|
||||
- Positioned in bottom-right corner
|
||||
- Clear visual feedback during switching
|
||||
- Native camera controls
|
||||
|
||||
```vue
|
||||
<button
|
||||
v-if="isNativePlatform"
|
||||
@click="toggleMobileCamera"
|
||||
class="camera-switch-btn"
|
||||
>
|
||||
<font-awesome icon="camera-rotate" />
|
||||
Switch Camera
|
||||
</button>
|
||||
```
|
||||
|
||||
2. **Desktop Interface**
|
||||
- Dropdown menu with all available cameras
|
||||
- Camera labels and device IDs
|
||||
- Real-time camera switching
|
||||
- Responsive design
|
||||
|
||||
```vue
|
||||
<select
|
||||
v-model="selectedCameraId"
|
||||
@change="onCameraChange"
|
||||
class="camera-select-dropdown"
|
||||
>
|
||||
<option
|
||||
v-for="camera in availableCameras"
|
||||
:key="camera.deviceId"
|
||||
:value="camera.deviceId"
|
||||
>
|
||||
{{ camera.label || `Camera ${camera.deviceId.slice(0, 4)}` }}
|
||||
</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
The camera switching implementation includes comprehensive error handling:
|
||||
|
||||
1. **Common Error Scenarios**
|
||||
- Camera in use by another application
|
||||
- Permission denied during switch
|
||||
- Device not available
|
||||
- Stream initialization failure
|
||||
- Camera switch timeout
|
||||
|
||||
2. **Error Response**
|
||||
```typescript
|
||||
private async handleCameraSwitch(deviceId: string): Promise<void> {
|
||||
try {
|
||||
this.updateCameraState("initializing", "Switching camera...");
|
||||
await this.switchCamera(deviceId);
|
||||
this.updateCameraState("active", "Camera switched successfully");
|
||||
} catch (error) {
|
||||
this.updateCameraState("error", "Failed to switch camera");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **User Feedback**
|
||||
- Visual indicators during switching
|
||||
- Error notifications
|
||||
- Camera state updates
|
||||
- Permission request dialogs
|
||||
|
||||
### State Management
|
||||
|
||||
The camera system maintains several states:
|
||||
|
||||
1. **Camera States**
|
||||
```typescript
|
||||
type CameraState =
|
||||
| "initializing" // Camera is being initialized
|
||||
| "ready" // Camera is ready to use
|
||||
| "active" // Camera is actively streaming
|
||||
| "in_use" // Camera is in use by another application
|
||||
| "permission_denied" // Camera permission was denied
|
||||
| "not_found" // No camera found on device
|
||||
| "error" // Generic error state
|
||||
| "off"; // Camera is off
|
||||
```
|
||||
|
||||
2. **State Transitions**
|
||||
- Initialization → Ready
|
||||
- Ready → Active
|
||||
- Active → Switching
|
||||
- Switching → Active/Error
|
||||
- Any state → Off (on cleanup)
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Camera Access**
|
||||
- Always check permissions before switching
|
||||
- Handle camera busy states
|
||||
- Implement proper cleanup
|
||||
- Monitor camera state changes
|
||||
|
||||
2. **Performance**
|
||||
- Optimize camera resolution
|
||||
- Handle stream switching efficiently
|
||||
- Manage memory usage
|
||||
- Implement proper cleanup
|
||||
|
||||
3. **User Experience**
|
||||
- Clear visual feedback
|
||||
- Smooth camera transitions
|
||||
- Intuitive camera controls
|
||||
- Responsive UI updates
|
||||
- Accessible camera selection
|
||||
|
||||
4. **Security**
|
||||
- Secure camera access
|
||||
- Permission management
|
||||
- Device validation
|
||||
- Stream security
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Test Scenarios**
|
||||
- Camera switching on both platforms
|
||||
- Permission handling
|
||||
- Error conditions
|
||||
- Multiple camera devices
|
||||
- Camera busy states
|
||||
- Stream initialization
|
||||
- UI responsiveness
|
||||
|
||||
2. **Test Environment**
|
||||
- Multiple mobile devices
|
||||
- Various desktop browsers
|
||||
- Different camera configurations
|
||||
- Network conditions
|
||||
- Permission states
|
||||
|
||||
### Capacitor Implementation Details
|
||||
|
||||
#### MLKit Barcode Scanner Configuration
|
||||
|
||||
1. **Plugin Setup**
|
||||
```typescript
|
||||
// capacitor.config.ts
|
||||
const config: CapacitorConfig = {
|
||||
plugins: {
|
||||
MLKitBarcodeScanner: {
|
||||
formats: ['QR_CODE'],
|
||||
detectorSize: 1.0,
|
||||
lensFacing: 'back',
|
||||
googleBarcodeScannerModuleInstallState: true,
|
||||
// Additional camera options
|
||||
cameraOptions: {
|
||||
quality: 0.8,
|
||||
allowEditing: false,
|
||||
resultType: 'uri',
|
||||
sourceType: 'CAMERA',
|
||||
saveToGallery: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. **Camera Management**
|
||||
```typescript
|
||||
// CapacitorQRScanner.ts
|
||||
export class CapacitorQRScanner implements QRScannerService {
|
||||
private currentLensFacing: LensFacing = LensFacing.Back;
|
||||
|
||||
async getAvailableCameras(): Promise<MediaDeviceInfo[]> {
|
||||
// On mobile, we have two fixed cameras
|
||||
return [
|
||||
{
|
||||
deviceId: 'back',
|
||||
label: 'Back Camera',
|
||||
kind: 'videoinput'
|
||||
},
|
||||
{
|
||||
deviceId: 'front',
|
||||
label: 'Front Camera',
|
||||
kind: 'videoinput'
|
||||
}
|
||||
] as MediaDeviceInfo[];
|
||||
}
|
||||
|
||||
async switchCamera(deviceId: string): Promise<void> {
|
||||
if (!this.isScanning) return;
|
||||
|
||||
const newLensFacing = deviceId === 'front' ?
|
||||
LensFacing.Front : LensFacing.Back;
|
||||
|
||||
// Stop current scan
|
||||
await this.stopScan();
|
||||
|
||||
// Update lens facing
|
||||
this.currentLensFacing = newLensFacing;
|
||||
|
||||
// Restart scan with new camera
|
||||
await this.startScan({
|
||||
camera: deviceId as 'front' | 'back'
|
||||
});
|
||||
}
|
||||
|
||||
async getCurrentCamera(): Promise<MediaDeviceInfo | null> {
|
||||
return {
|
||||
deviceId: this.currentLensFacing === LensFacing.Front ? 'front' : 'back',
|
||||
label: this.currentLensFacing === LensFacing.Front ?
|
||||
'Front Camera' : 'Back Camera',
|
||||
kind: 'videoinput'
|
||||
} as MediaDeviceInfo;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Camera State Management**
|
||||
```typescript
|
||||
// CapacitorQRScanner.ts
|
||||
private async handleCameraState(): Promise<void> {
|
||||
try {
|
||||
// Check if camera is available
|
||||
const { camera } = await BarcodeScanner.checkPermissions();
|
||||
|
||||
if (camera === 'denied') {
|
||||
this.updateCameraState('permission_denied');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if camera is in use
|
||||
const isInUse = await this.isCameraInUse();
|
||||
if (isInUse) {
|
||||
this.updateCameraState('in_use');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateCameraState('ready');
|
||||
} catch (error) {
|
||||
this.updateCameraState('error', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async isCameraInUse(): Promise<boolean> {
|
||||
try {
|
||||
// Try to start a test scan
|
||||
await BarcodeScanner.startScan({
|
||||
formats: [BarcodeFormat.QrCode],
|
||||
lensFacing: this.currentLensFacing
|
||||
});
|
||||
// If successful, stop it immediately
|
||||
await BarcodeScanner.stopScan();
|
||||
return false;
|
||||
} catch (error) {
|
||||
return error.message.includes('camera in use');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Error Handling**
|
||||
```typescript
|
||||
// CapacitorQRScanner.ts
|
||||
private async handleCameraError(error: Error): Promise<void> {
|
||||
switch (error.name) {
|
||||
case 'CameraPermissionDenied':
|
||||
this.updateCameraState('permission_denied');
|
||||
break;
|
||||
case 'CameraInUse':
|
||||
this.updateCameraState('in_use');
|
||||
break;
|
||||
case 'CameraUnavailable':
|
||||
this.updateCameraState('not_found');
|
||||
break;
|
||||
default:
|
||||
this.updateCameraState('error', error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Platform-Specific Considerations
|
||||
|
||||
1. **iOS Implementation**
|
||||
- Camera permissions in Info.plist
|
||||
- Privacy descriptions
|
||||
- Camera usage description
|
||||
- Background camera access
|
||||
|
||||
```xml
|
||||
<!-- ios/App/App/Info.plist -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need access to your camera to scan QR codes</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need access to save scanned QR codes</string>
|
||||
```
|
||||
|
||||
2. **Android Implementation**
|
||||
- Camera permissions in AndroidManifest.xml
|
||||
- Runtime permission handling
|
||||
- Camera features declaration
|
||||
- Hardware feature requirements
|
||||
|
||||
```xml
|
||||
<!-- android/app/src/main/AndroidManifest.xml -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
```
|
||||
|
||||
3. **Platform-Specific Features**
|
||||
- iOS: Camera orientation handling
|
||||
- Android: Camera resolution optimization
|
||||
- Both: Battery usage optimization
|
||||
- Both: Memory management
|
||||
|
||||
```typescript
|
||||
// Platform-specific optimizations
|
||||
private getPlatformSpecificOptions(): StartScanOptions {
|
||||
const baseOptions: StartScanOptions = {
|
||||
formats: [BarcodeFormat.QrCode],
|
||||
lensFacing: this.currentLensFacing
|
||||
};
|
||||
|
||||
if (Capacitor.getPlatform() === 'ios') {
|
||||
return {
|
||||
...baseOptions,
|
||||
// iOS-specific options
|
||||
cameraOptions: {
|
||||
quality: 0.7, // Lower quality for better performance
|
||||
allowEditing: false,
|
||||
resultType: 'uri'
|
||||
}
|
||||
};
|
||||
} else if (Capacitor.getPlatform() === 'android') {
|
||||
return {
|
||||
...baseOptions,
|
||||
// Android-specific options
|
||||
cameraOptions: {
|
||||
quality: 0.8,
|
||||
allowEditing: false,
|
||||
resultType: 'uri',
|
||||
saveToGallery: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return baseOptions;
|
||||
}
|
||||
```
|
||||
|
||||
#### Performance Optimization
|
||||
|
||||
1. **Battery Usage**
|
||||
```typescript
|
||||
// CapacitorQRScanner.ts
|
||||
private optimizeBatteryUsage(): void {
|
||||
// Reduce scan frequency when battery is low
|
||||
if (this.isLowBattery()) {
|
||||
this.scanInterval = 2000; // 2 seconds between scans
|
||||
} else {
|
||||
this.scanInterval = 1000; // 1 second between scans
|
||||
}
|
||||
}
|
||||
|
||||
private isLowBattery(): boolean {
|
||||
// Check battery level if available
|
||||
if (Capacitor.isPluginAvailable('Battery')) {
|
||||
const { level } = await Battery.getBatteryLevel();
|
||||
return level < 0.2; // 20% or lower
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Memory Management**
|
||||
```typescript
|
||||
// CapacitorQRScanner.ts
|
||||
private async cleanupResources(): Promise<void> {
|
||||
// Stop scanning
|
||||
await this.stopScan();
|
||||
|
||||
// Clear any stored camera data
|
||||
this.currentLensFacing = LensFacing.Back;
|
||||
|
||||
// Remove listeners
|
||||
this.listenerHandles.forEach(handle => handle());
|
||||
this.listenerHandles = [];
|
||||
|
||||
// Reset state
|
||||
this.isScanning = false;
|
||||
this.updateCameraState('off');
|
||||
}
|
||||
```
|
||||
|
||||
#### Testing on Capacitor
|
||||
|
||||
1. **Device Testing**
|
||||
- Test on multiple iOS devices
|
||||
- Test on multiple Android devices
|
||||
- Test different camera configurations
|
||||
- Test with different screen sizes
|
||||
- Test with different OS versions
|
||||
|
||||
2. **Camera Testing**
|
||||
- Test front camera switching
|
||||
- Test back camera switching
|
||||
- Test camera permissions
|
||||
- Test camera in use scenarios
|
||||
- Test low light conditions
|
||||
- Test different QR code sizes
|
||||
- Test different QR code distances
|
||||
|
||||
3. **Performance Testing**
|
||||
- Battery usage monitoring
|
||||
- Memory usage monitoring
|
||||
- Camera switching speed
|
||||
- QR code detection speed
|
||||
- App responsiveness
|
||||
- Background/foreground transitions
|
||||
339
doc/secure-storage-implementation.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# Secure Storage Implementation Guide for TimeSafari App
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:
|
||||
|
||||
1. **Platform-Specific Storage Solutions**:
|
||||
- Web: SQLite with IndexedDB backend (absurd-sql)
|
||||
- Electron: SQLite with Node.js backend
|
||||
- Native: (Planned) SQLCipher with platform-specific secure storage
|
||||
|
||||
2. **Key Features**:
|
||||
- SQLite-based storage using absurd-sql for web
|
||||
- Platform-specific service factory pattern
|
||||
- Consistent API across platforms
|
||||
- Migration support from Dexie.js
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
```bash
|
||||
# Core dependencies
|
||||
npm install @jlongster/sql.js
|
||||
npm install absurd-sql
|
||||
|
||||
# Platform-specific dependencies (for future native support)
|
||||
npm install @capacitor/preferences
|
||||
npm install @capacitor-community/biometric-auth
|
||||
```
|
||||
|
||||
### 2. Basic Usage
|
||||
|
||||
```typescript
|
||||
// Using the platform service
|
||||
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';
|
||||
|
||||
// Get platform-specific service instance
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
|
||||
// Example database operations
|
||||
async function example() {
|
||||
try {
|
||||
// Query example
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT * FROM accounts WHERE did = ?",
|
||||
[did]
|
||||
);
|
||||
|
||||
// Execute example
|
||||
await platformService.dbExec(
|
||||
"INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
|
||||
[did, publicKeyHex]
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Database operation failed:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Platform Detection
|
||||
|
||||
```typescript
|
||||
// src/services/PlatformServiceFactory.ts
|
||||
export class PlatformServiceFactory {
|
||||
static getInstance(): PlatformService {
|
||||
if (process.env.ELECTRON) {
|
||||
// Electron platform
|
||||
return new ElectronPlatformService();
|
||||
} else {
|
||||
// Web platform (default)
|
||||
return new AbsurdSqlDatabaseService();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Current Implementation Details
|
||||
|
||||
#### Web Platform (AbsurdSqlDatabaseService)
|
||||
|
||||
The web platform uses absurd-sql with IndexedDB backend:
|
||||
|
||||
```typescript
|
||||
// src/services/AbsurdSqlDatabaseService.ts
|
||||
export class AbsurdSqlDatabaseService implements PlatformService {
|
||||
private static instance: AbsurdSqlDatabaseService | null = null;
|
||||
private db: AbsurdSqlDatabase | null = null;
|
||||
private initialized: boolean = false;
|
||||
|
||||
// Singleton pattern
|
||||
static getInstance(): AbsurdSqlDatabaseService {
|
||||
if (!AbsurdSqlDatabaseService.instance) {
|
||||
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
||||
}
|
||||
return AbsurdSqlDatabaseService.instance;
|
||||
}
|
||||
|
||||
// Database operations
|
||||
async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
||||
}
|
||||
|
||||
async dbExec(sql: string, params: unknown[] = []): Promise<void> {
|
||||
await this.waitForInitialization();
|
||||
await this.queueOperation<void>("run", sql, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key features:
|
||||
- Uses absurd-sql for SQLite in the browser
|
||||
- Implements operation queuing for thread safety
|
||||
- Handles initialization and connection management
|
||||
- Provides consistent API across platforms
|
||||
|
||||
### 5. Migration from Dexie.js
|
||||
|
||||
The current implementation supports gradual migration from Dexie.js:
|
||||
|
||||
```typescript
|
||||
// Example of dual-storage pattern
|
||||
async function getAccount(did: string): Promise<Account | undefined> {
|
||||
// Try SQLite first
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
let account = await platform.dbQuery(
|
||||
"SELECT * FROM accounts WHERE did = ?",
|
||||
[did]
|
||||
);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
account = await db.accounts.get(did);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
```
|
||||
|
||||
#### A. Modifying Code
|
||||
|
||||
When converting from Dexie.js to SQL-based implementation, follow these patterns:
|
||||
|
||||
1. **Database Access Pattern**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
const result = await db.table.where("field").equals(value).first();
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
let result = await platform.dbQuery(
|
||||
"SELECT * FROM table WHERE field = ?",
|
||||
[value]
|
||||
);
|
||||
result = databaseUtil.mapQueryResultToValues(result);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
result = await db.table.where("field").equals(value).first();
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update Operations**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
await db.table.where("id").equals(id).modify(changes);
|
||||
|
||||
// After (SQL)
|
||||
// For settings updates, use the utility methods:
|
||||
await databaseUtil.updateDefaultSettings(changes);
|
||||
// OR
|
||||
await databaseUtil.updateAccountSettings(did, changes);
|
||||
|
||||
// For other tables, use direct SQL:
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
await platform.dbExec(
|
||||
"UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
|
||||
[changes.field1, changes.field2, id]
|
||||
);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.table.where("id").equals(id).modify(changes);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Insert Operations**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
await db.table.add(item);
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const columns = Object.keys(item);
|
||||
const values = Object.values(item);
|
||||
const placeholders = values.map(() => '?').join(', ');
|
||||
const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
await platform.dbExec(sql, values);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.table.add(item);
|
||||
}
|
||||
```
|
||||
|
||||
4. **Delete Operations**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
await db.table.where("id").equals(id).delete();
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.table.where("id").equals(id).delete();
|
||||
}
|
||||
```
|
||||
|
||||
5. **Result Processing**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
const items = await db.table.toArray();
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
let items = await platform.dbQuery("SELECT * FROM table");
|
||||
items = databaseUtil.mapQueryResultToValues(items);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
items = await db.table.toArray();
|
||||
}
|
||||
```
|
||||
|
||||
6. **Using Utility Methods**
|
||||
|
||||
When working with settings or other common operations, use the utility methods in `db/index.ts`:
|
||||
|
||||
```typescript
|
||||
// Settings operations
|
||||
await databaseUtil.updateDefaultSettings(settings);
|
||||
await databaseUtil.updateAccountSettings(did, settings);
|
||||
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
|
||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
|
||||
// Logging operations
|
||||
await databaseUtil.logToDb(message);
|
||||
await databaseUtil.logConsoleAndDb(message, showInConsole);
|
||||
```
|
||||
|
||||
Key Considerations:
|
||||
- Always use `databaseUtil.mapQueryResultToValues()` to process SQL query results
|
||||
- Use utility methods from `db/index.ts` when available instead of direct SQL
|
||||
- Keep Dexie fallbacks wrapped in `if (USE_DEXIE_DB)` checks
|
||||
- For queries that return results, use `let` variables to allow Dexie fallback to override
|
||||
- For updates/inserts/deletes, execute both SQL and Dexie operations when `USE_DEXIE_DB` is true
|
||||
|
||||
Example Migration:
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
export async function updateSettings(settings: Settings): Promise<void> {
|
||||
await db.settings.put(settings);
|
||||
}
|
||||
|
||||
// After (SQL)
|
||||
export async function updateSettings(settings: Settings): Promise<void> {
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const { sql, params } = generateUpdateStatement(
|
||||
settings,
|
||||
"settings",
|
||||
"id = ?",
|
||||
[settings.id]
|
||||
);
|
||||
await platform.dbExec(sql, params);
|
||||
}
|
||||
```
|
||||
|
||||
Remember to:
|
||||
- Create database access code to use the platform service, putting it in front of the Dexie version
|
||||
- Instead of removing Dexie-specific code, keep it.
|
||||
|
||||
- For creates & updates & deletes, the duplicate code is fine.
|
||||
|
||||
- For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if
|
||||
it's true then use that result instead of the SQL code's result.
|
||||
|
||||
- Consider data migration needs, and warn if there are any potential migration problems
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Functionality**
|
||||
- [x] Basic CRUD operations work correctly
|
||||
- [x] Platform service factory pattern implemented
|
||||
- [x] Error handling in place
|
||||
- [ ] Native platform support (planned)
|
||||
|
||||
2. **Performance**
|
||||
- [x] Database operations complete within acceptable time
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Performance monitoring (planned)
|
||||
|
||||
3. **Security**
|
||||
- [x] Basic data integrity
|
||||
- [ ] Encryption (planned for native platforms)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Platform-specific security features (planned)
|
||||
|
||||
4. **Testing**
|
||||
- [x] Basic unit tests
|
||||
- [ ] Comprehensive integration tests (planned)
|
||||
- [ ] Platform-specific tests (planned)
|
||||
- [ ] Migration tests (planned)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Native Platform Support**
|
||||
- Implement SQLCipher for iOS/Android
|
||||
- Add platform-specific secure storage
|
||||
- Implement biometric authentication
|
||||
|
||||
2. **Enhanced Security**
|
||||
- Add encryption for sensitive data
|
||||
- Implement secure key storage
|
||||
- Add platform-specific security features
|
||||
|
||||
3. **Testing and Monitoring**
|
||||
- Add comprehensive test coverage
|
||||
- Implement performance monitoring
|
||||
- Add error tracking and analytics
|
||||
|
||||
4. **Documentation**
|
||||
- Add API documentation
|
||||
- Create migration guides
|
||||
- Document security measures
|
||||
329
doc/storage-implementation-checklist.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Storage Implementation Checklist
|
||||
|
||||
## Core Services
|
||||
|
||||
### 1. Storage Service Layer
|
||||
- [x] Create base `PlatformService` interface
|
||||
- [x] Define common methods for all platforms
|
||||
- [x] Add platform-specific method signatures
|
||||
- [x] Include error handling types
|
||||
- [x] Add migration support methods
|
||||
|
||||
- [x] Implement platform-specific services
|
||||
- [x] `AbsurdSqlDatabaseService` (web)
|
||||
- [x] Database initialization
|
||||
- [x] VFS setup with IndexedDB backend
|
||||
- [x] Connection management
|
||||
- [x] Operation queuing
|
||||
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
||||
- [ ] SQLCipher integration
|
||||
- [ ] Native bridge setup
|
||||
- [ ] File system access
|
||||
- [ ] `ElectronSQLiteService` (planned)
|
||||
- [ ] Node SQLite integration
|
||||
- [ ] IPC communication
|
||||
- [ ] File system access
|
||||
|
||||
### 2. Migration Services
|
||||
- [x] Implement basic migration support
|
||||
- [x] Dual-storage pattern (SQLite + Dexie)
|
||||
- [x] Basic data verification
|
||||
- [ ] Rollback procedures (planned)
|
||||
- [ ] Progress tracking (planned)
|
||||
- [ ] Create `MigrationUI` components (planned)
|
||||
- [ ] Progress indicators
|
||||
- [ ] Error handling
|
||||
- [ ] User notifications
|
||||
- [ ] Manual triggers
|
||||
|
||||
### 3. Security Layer
|
||||
- [x] Basic data integrity
|
||||
- [ ] Implement `EncryptionService` (planned)
|
||||
- [ ] Key management
|
||||
- [ ] Encryption/decryption
|
||||
- [ ] Secure storage
|
||||
- [ ] Add `BiometricService` (planned)
|
||||
- [ ] Platform detection
|
||||
- [ ] Authentication flow
|
||||
- [ ] Fallback mechanisms
|
||||
|
||||
## Platform-Specific Implementation
|
||||
|
||||
### Web Platform
|
||||
- [x] Setup absurd-sql
|
||||
- [x] Install dependencies
|
||||
```json
|
||||
{
|
||||
"@jlongster/sql.js": "^1.8.0",
|
||||
"absurd-sql": "^1.8.0"
|
||||
}
|
||||
```
|
||||
- [x] Configure VFS with IndexedDB backend
|
||||
- [x] Setup worker threads
|
||||
- [x] Implement operation queuing
|
||||
- [x] Configure database pragmas
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode=MEMORY;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA busy_timeout=5000;
|
||||
```
|
||||
|
||||
- [x] Update build configuration
|
||||
- [x] Modify `vite.config.ts`
|
||||
- [x] Add worker configuration
|
||||
- [x] Update chunk splitting
|
||||
- [x] Configure asset handling
|
||||
|
||||
- [x] Implement IndexedDB backend
|
||||
- [x] Create database service
|
||||
- [x] Add operation queuing
|
||||
- [x] Handle initialization
|
||||
- [x] Implement atomic operations
|
||||
|
||||
### iOS Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Install pod dependencies
|
||||
- [ ] Configure encryption
|
||||
- [ ] Setup keychain access
|
||||
- [ ] Implement secure storage
|
||||
|
||||
- [ ] Update Capacitor config
|
||||
- [ ] Modify `capacitor.config.ts`
|
||||
- [ ] Add iOS permissions
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup app groups
|
||||
|
||||
### Android Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Add Gradle dependencies
|
||||
- [ ] Configure encryption
|
||||
- [ ] Setup keystore
|
||||
- [ ] Implement secure storage
|
||||
|
||||
- [ ] Update Capacitor config
|
||||
- [ ] Modify `capacitor.config.ts`
|
||||
- [ ] Add Android permissions
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup file provider
|
||||
|
||||
### Electron Platform (Planned)
|
||||
- [ ] Setup Node SQLite
|
||||
- [ ] Install dependencies
|
||||
- [ ] Configure IPC
|
||||
- [ ] Setup file system access
|
||||
- [ ] Implement secure storage
|
||||
|
||||
- [ ] Update Electron config
|
||||
- [ ] Modify `electron.config.ts`
|
||||
- [ ] Add security policies
|
||||
- [ ] Configure file access
|
||||
- [ ] Setup auto-updates
|
||||
|
||||
## Data Models and Types
|
||||
|
||||
### 1. Database Schema
|
||||
- [x] Define tables
|
||||
|
||||
```sql
|
||||
-- Accounts table
|
||||
CREATE TABLE accounts (
|
||||
did TEXT PRIMARY KEY,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Settings table
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Contacts table
|
||||
CREATE TABLE contacts (
|
||||
id TEXT PRIMARY KEY,
|
||||
did TEXT NOT NULL,
|
||||
name TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (did) REFERENCES accounts(did)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
||||
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||
```
|
||||
|
||||
- [x] Create indexes
|
||||
- [x] Define constraints
|
||||
- [ ] Add triggers (planned)
|
||||
- [ ] Setup migrations (planned)
|
||||
|
||||
### 2. Type Definitions
|
||||
|
||||
- [x] Create interfaces
|
||||
```typescript
|
||||
interface Account {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface Setting {
|
||||
key: string;
|
||||
value: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
id: string;
|
||||
did: string;
|
||||
name?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] Add validation
|
||||
- [x] Create DTOs
|
||||
- [x] Define enums
|
||||
- [x] Add type guards
|
||||
|
||||
## UI Components
|
||||
|
||||
### 1. Migration UI (Planned)
|
||||
- [ ] Create components
|
||||
- [ ] `MigrationProgress.vue`
|
||||
- [ ] `MigrationError.vue`
|
||||
- [ ] `MigrationSettings.vue`
|
||||
- [ ] `MigrationStatus.vue`
|
||||
|
||||
### 2. Settings UI (Planned)
|
||||
- [ ] Update components
|
||||
- [ ] Add storage settings
|
||||
- [ ] Add migration controls
|
||||
- [ ] Add backup options
|
||||
- [ ] Add security settings
|
||||
|
||||
### 3. Error Handling UI (Planned)
|
||||
- [ ] Create components
|
||||
- [ ] `StorageError.vue`
|
||||
- [ ] `QuotaExceeded.vue`
|
||||
- [ ] `MigrationFailed.vue`
|
||||
- [ ] `RecoveryOptions.vue`
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Unit Tests
|
||||
- [x] Basic service tests
|
||||
- [x] Platform service tests
|
||||
- [x] Database operation tests
|
||||
- [ ] Security service tests (planned)
|
||||
- [ ] Platform detection tests (planned)
|
||||
|
||||
### 2. Integration Tests (Planned)
|
||||
- [ ] Test migrations
|
||||
- [ ] Web platform tests
|
||||
- [ ] iOS platform tests
|
||||
- [ ] Android platform tests
|
||||
- [ ] Electron platform tests
|
||||
|
||||
### 3. E2E Tests (Planned)
|
||||
- [ ] Test workflows
|
||||
- [ ] Account management
|
||||
- [ ] Settings management
|
||||
- [ ] Contact management
|
||||
- [ ] Migration process
|
||||
|
||||
## Documentation
|
||||
|
||||
### 1. Technical Documentation
|
||||
- [x] Update architecture docs
|
||||
- [x] Add API documentation
|
||||
- [ ] Create migration guides (planned)
|
||||
- [ ] Document security measures (planned)
|
||||
|
||||
### 2. User Documentation (Planned)
|
||||
- [ ] Update user guides
|
||||
- [ ] Add troubleshooting guides
|
||||
- [ ] Create FAQ
|
||||
- [ ] Document new features
|
||||
|
||||
## Deployment
|
||||
|
||||
### 1. Build Process
|
||||
- [x] Update build scripts
|
||||
- [x] Add platform-specific builds
|
||||
- [ ] Configure CI/CD (planned)
|
||||
- [ ] Setup automated testing (planned)
|
||||
|
||||
### 2. Release Process (Planned)
|
||||
- [ ] Create release checklist
|
||||
- [ ] Add version management
|
||||
- [ ] Setup rollback procedures
|
||||
- [ ] Configure monitoring
|
||||
|
||||
## Monitoring and Analytics (Planned)
|
||||
|
||||
### 1. Error Tracking
|
||||
- [ ] Setup error logging
|
||||
- [ ] Add performance monitoring
|
||||
- [ ] Configure alerts
|
||||
- [ ] Create dashboards
|
||||
|
||||
### 2. Usage Analytics
|
||||
- [ ] Add storage metrics
|
||||
- [ ] Track migration success
|
||||
- [ ] Monitor performance
|
||||
- [ ] Collect user feedback
|
||||
|
||||
## Security Audit (Planned)
|
||||
|
||||
### 1. Code Review
|
||||
- [ ] Review encryption
|
||||
- [ ] Check access controls
|
||||
- [ ] Verify data handling
|
||||
- [ ] Audit dependencies
|
||||
|
||||
### 2. Penetration Testing
|
||||
- [ ] Test data access
|
||||
- [ ] Verify encryption
|
||||
- [ ] Check authentication
|
||||
- [ ] Review permissions
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### 1. Performance
|
||||
- [x] Query response time < 100ms
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Migration time < 5s per 1000 records (planned)
|
||||
- [ ] Storage overhead < 10% (planned)
|
||||
- [ ] Memory usage < 50MB (planned)
|
||||
|
||||
### 2. Reliability
|
||||
- [x] Basic data integrity
|
||||
- [x] Operation queuing
|
||||
- [ ] Automatic recovery (planned)
|
||||
- [ ] Backup verification (planned)
|
||||
- [ ] Transaction atomicity (planned)
|
||||
- [ ] Data consistency (planned)
|
||||
|
||||
### 3. Security
|
||||
- [x] Basic data integrity
|
||||
- [ ] AES-256 encryption (planned)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Access control (planned)
|
||||
- [ ] Audit logging (planned)
|
||||
|
||||
### 4. User Experience
|
||||
- [x] Basic database operations
|
||||
- [ ] Smooth migration (planned)
|
||||
- [ ] Clear error messages (planned)
|
||||
- [ ] Progress indicators (planned)
|
||||
- [ ] Recovery options (planned)
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>TimeSafari</title>
|
||||
</head>
|
||||
|
||||
26
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
|
||||
# User-specific Xcode files
|
||||
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
||||
App/App.xcodeproj/*.xcuserstate
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
|
||||
App/App/Assets.xcassets/AppIcon.appiconset
|
||||
App/App/Assets.xcassets/Splash.imageset
|
||||
477
ios/App/App.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,477 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
4B546315E668C7A13939F417 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
||||
4B546315E668C7A13939F417 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
|
||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastUpgradeCheck = 1630;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Fix Privacy Manifest";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Fix Privacy Manifest";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 7XVXYPEQYJ;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 7XVXYPEQYJ;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
10
ios/App/App.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:App.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
54
ios/App/App/AppDelegate.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import CapacitorCommunitySqlite
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Initialize SQLite
|
||||
//let sqlite = SQLite()
|
||||
//sqlite.initialize()
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
6
ios/App/App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
32
ios/App/App/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
19
ios/App/App/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
62
ios/App/App/Info.plist
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>TimeSafari</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Time Safari allows you to take photos, and also scan QR codes from contacts.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Time Safari allows you to upload photos.</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>app.timesafari</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
20
ios/App/App/entitlements.mac.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.debugger</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.addressbook</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.calendars</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
35
ios/App/Podfile
Normal file
@@ -0,0 +1,35 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '13.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||
# Requires CocoaPods 1.6 or newer
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
||||
end
|
||||
end
|
||||
end
|
||||
162
ios/App/Podfile.lock
Normal file
@@ -0,0 +1,162 @@
|
||||
PODS:
|
||||
- Capacitor (6.2.1):
|
||||
- CapacitorCordova
|
||||
- CapacitorApp (6.0.2):
|
||||
- Capacitor
|
||||
- CapacitorCamera (6.1.2):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (6.0.2):
|
||||
- Capacitor
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
- CapacitorCordova (6.2.1)
|
||||
- CapacitorFilesystem (6.0.3):
|
||||
- Capacitor
|
||||
- CapacitorMlkitBarcodeScanning (6.2.0):
|
||||
- Capacitor
|
||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
||||
- CapacitorShare (6.0.3):
|
||||
- Capacitor
|
||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
||||
- Capacitor
|
||||
- GoogleDataTransport (9.4.1):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleMLKit/BarcodeScanning (5.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 4.0.0)
|
||||
- GoogleMLKit/MLKitCore (5.0.0):
|
||||
- MLKitCommon (~> 10.0.0)
|
||||
- GoogleToolboxForMac/DebugUtils (2.3.2):
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- GoogleToolboxForMac/Defines (2.3.2)
|
||||
- GoogleToolboxForMac/Logger (2.3.2):
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- "GoogleToolboxForMac/NSData+zlib (2.3.2)":
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)":
|
||||
- GoogleToolboxForMac/DebugUtils (= 2.3.2)
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)"
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (2.3.2)"
|
||||
- GoogleUtilities/Environment (7.13.3):
|
||||
- GoogleUtilities/Privacy
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/Logger (7.13.3):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (7.13.3)
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilitiesComponents (1.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- MLImage (1.0.0-beta5)
|
||||
- MLKitBarcodeScanning (4.0.0):
|
||||
- MLKitCommon (~> 10.0)
|
||||
- MLKitVision (~> 6.0)
|
||||
- MLKitCommon (10.0.0):
|
||||
- GoogleDataTransport (~> 9.0)
|
||||
- GoogleToolboxForMac/Logger (~> 2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
|
||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
|
||||
- GoogleUtilities/UserDefaults (~> 7.0)
|
||||
- GoogleUtilitiesComponents (~> 1.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
|
||||
- MLKitVision (6.0.0):
|
||||
- GoogleToolboxForMac/Logger (~> 2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
|
||||
- MLImage (= 1.0.0-beta5)
|
||||
- MLKitCommon (~> 10.0)
|
||||
- nanopb (2.30910.0):
|
||||
- nanopb/decode (= 2.30910.0)
|
||||
- nanopb/encode (= 2.30910.0)
|
||||
- nanopb/decode (2.30910.0)
|
||||
- nanopb/encode (2.30910.0)
|
||||
- PromisesObjC (2.4.0)
|
||||
- SQLCipher (4.9.0):
|
||||
- SQLCipher/standard (= 4.9.0)
|
||||
- SQLCipher/common (4.9.0)
|
||||
- SQLCipher/standard (4.9.0):
|
||||
- SQLCipher/common
|
||||
- ZIPFoundation (0.9.19)
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- GoogleDataTransport
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GoogleUtilitiesComponents
|
||||
- GTMSessionFetcher
|
||||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorApp:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/@capacitor/camera"
|
||||
CapacitorCommunitySqlite:
|
||||
:path: "../../node_modules/@capacitor-community/sqlite"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorFilesystem:
|
||||
:path: "../../node_modules/@capacitor/filesystem"
|
||||
CapacitorMlkitBarcodeScanning:
|
||||
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/@capacitor/share"
|
||||
CapawesomeCapacitorFilePicker:
|
||||
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
||||
GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
||||
MLKitBarcodeScanning: 9cb0ec5ec65bbb5db31de4eba0a3289626beab4e
|
||||
MLKitCommon: afcd11b6c0735066a0dde8b4bf2331f6197cbca2
|
||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
5
ios/App/app_privacy_manifest_fixer/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Build
|
||||
/Build/
|
||||
58
ios/App/app_privacy_manifest_fixer/CHANGELOG.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## 1.4.1
|
||||
- Fix macOS app re-signing issue.
|
||||
- Automatically enable Hardened Runtime in macOS codesign.
|
||||
- Add clean script.
|
||||
|
||||
## 1.4.0
|
||||
- Support for macOS app ([#9](https://github.com/crasowas/app_privacy_manifest_fixer/issues/9)).
|
||||
|
||||
## 1.3.11
|
||||
- Fix install issue by skipping `PBXAggregateTarget` ([#4](https://github.com/crasowas/app_privacy_manifest_fixer/issues/4)).
|
||||
|
||||
## 1.3.10
|
||||
- Fix app re-signing issue.
|
||||
- Enhance Build Phases script robustness.
|
||||
|
||||
## 1.3.9
|
||||
- Add log file output.
|
||||
|
||||
## 1.3.8
|
||||
- Add version info to privacy access report.
|
||||
- Remove empty tables from privacy access report.
|
||||
|
||||
## 1.3.7
|
||||
- Enhance API symbols analysis with strings tool.
|
||||
- Improve performance of API usage analysis.
|
||||
|
||||
## 1.3.5
|
||||
- Fix issue with inaccurate privacy manifest search.
|
||||
- Disable dependency analysis to force the script to run on every build.
|
||||
- Add placeholder for privacy access report.
|
||||
- Update build output directory naming convention.
|
||||
- Add examples for privacy access report.
|
||||
|
||||
## 1.3.0
|
||||
- Add privacy access report generation.
|
||||
|
||||
## 1.2.3
|
||||
- Fix issue with relative path parameter.
|
||||
- Add support for all application targets.
|
||||
|
||||
## 1.2.1
|
||||
- Fix backup issue with empty user templates directory.
|
||||
|
||||
## 1.2.0
|
||||
- Add uninstall script.
|
||||
|
||||
## 1.1.2
|
||||
- Remove `Templates/.gitignore` to track `UserTemplates`.
|
||||
- Fix incorrect use of `App.xcprivacy` template in `App.framework`.
|
||||
|
||||
## 1.1.0
|
||||
- Add logs for latest release fetch failure.
|
||||
- Fix issue with converting published time to local time.
|
||||
- Disable showing environment variables in the build log.
|
||||
- Add `--install-builds-only` command line option.
|
||||
|
||||
## 1.0.0
|
||||
- Initial version.
|
||||
80
ios/App/app_privacy_manifest_fixer/Common/constants.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2025, crasowas.
|
||||
#
|
||||
# Use of this source code is governed by a MIT-style license
|
||||
# that can be found in the LICENSE file or at
|
||||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
set -e
|
||||
|
||||
# Prevent duplicate loading
|
||||
if [ -n "$CONSTANTS_SH_LOADED" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
readonly CONSTANTS_SH_LOADED=1
|
||||
|
||||
# File name of the privacy manifest
|
||||
readonly PRIVACY_MANIFEST_FILE_NAME="PrivacyInfo.xcprivacy"
|
||||
|
||||
# Common privacy manifest template file names
|
||||
readonly APP_TEMPLATE_FILE_NAME="AppTemplate.xcprivacy"
|
||||
readonly FRAMEWORK_TEMPLATE_FILE_NAME="FrameworkTemplate.xcprivacy"
|
||||
|
||||
# Universal delimiter
|
||||
readonly DELIMITER=":"
|
||||
|
||||
# Space escape symbol for handling space in path
|
||||
readonly SPACE_ESCAPE="\u0020"
|
||||
|
||||
# Default value when the version cannot be retrieved
|
||||
readonly UNKNOWN_VERSION="unknown"
|
||||
|
||||
# Categories of required reason APIs
|
||||
readonly API_CATEGORIES=(
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp"
|
||||
"NSPrivacyAccessedAPICategorySystemBootTime"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace"
|
||||
"NSPrivacyAccessedAPICategoryActiveKeyboards"
|
||||
"NSPrivacyAccessedAPICategoryUserDefaults"
|
||||
)
|
||||
|
||||
# Symbol of the required reason APIs and their categories
|
||||
#
|
||||
# See also:
|
||||
# * https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api
|
||||
# * https://github.com/Wooder/ios_17_required_reason_api_scanner/blob/main/required_reason_api_binary_scanner.sh
|
||||
readonly API_SYMBOLS=(
|
||||
# NSPrivacyAccessedAPICategoryFileTimestamp
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlist"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistbulk"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fgetattrlist"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}stat"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstat"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstatat"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}lstat"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistat"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileCreationDate"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileModificationDate"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLContentModificationDateKey"
|
||||
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLCreationDateKey"
|
||||
# NSPrivacyAccessedAPICategorySystemBootTime
|
||||
"NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}systemUptime"
|
||||
"NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}mach_absolute_time"
|
||||
# NSPrivacyAccessedAPICategoryDiskSpace
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statfs"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statvfs"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatfs"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatvfs"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemFreeSize"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemSize"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityKey"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForImportantUsageKey"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForOpportunisticUsageKey"
|
||||
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeTotalCapacityKey"
|
||||
# NSPrivacyAccessedAPICategoryActiveKeyboards
|
||||
"NSPrivacyAccessedAPICategoryActiveKeyboards${DELIMITER}activeInputModes"
|
||||
# NSPrivacyAccessedAPICategoryUserDefaults
|
||||
"NSPrivacyAccessedAPICategoryUserDefaults${DELIMITER}NSUserDefaults"
|
||||
)
|
||||
125
ios/App/app_privacy_manifest_fixer/Common/utils.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2025, crasowas.
|
||||
#
|
||||
# Use of this source code is governed by a MIT-style license
|
||||
# that can be found in the LICENSE file or at
|
||||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
set -e
|
||||
|
||||
# Prevent duplicate loading
|
||||
if [ -n "$UTILS_SH_LOADED" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
readonly UTILS_SH_LOADED=1
|
||||
|
||||
# Absolute path of the script and the tool's root directory
|
||||
script_path="$(realpath "${BASH_SOURCE[0]}")"
|
||||
tool_root_path="$(dirname "$(dirname "$script_path")")"
|
||||
|
||||
# Load common constants
|
||||
source "$tool_root_path/Common/constants.sh"
|
||||
|
||||
# Print the elements of an array along with their indices
|
||||
function print_array() {
|
||||
local -a array=("$@")
|
||||
|
||||
for ((i=0; i<${#array[@]}; i++)); do
|
||||
echo "[$i] $(decode_path "${array[i]}")"
|
||||
done
|
||||
}
|
||||
|
||||
# Split a string into substrings using a specified delimiter
|
||||
function split_string_by_delimiter() {
|
||||
local string="$1"
|
||||
local -a substrings=()
|
||||
|
||||
IFS="$DELIMITER" read -ra substrings <<< "$string"
|
||||
|
||||
echo "${substrings[@]}"
|
||||
}
|
||||
|
||||
# Encode a path string by replacing space with an escape character
|
||||
function encode_path() {
|
||||
echo "$1" | sed "s/ /$SPACE_ESCAPE/g"
|
||||
}
|
||||
|
||||
# Decode a path string by replacing encoded character with space
|
||||
function decode_path() {
|
||||
echo "$1" | sed "s/$SPACE_ESCAPE/ /g"
|
||||
}
|
||||
|
||||
# Get the dependency name by removing common suffixes
|
||||
function get_dependency_name() {
|
||||
local path="$1"
|
||||
|
||||
local dir_name="$(basename "$path")"
|
||||
# Remove `.app`, `.framework`, and `.xcframework` suffixes
|
||||
local dep_name="${dir_name%.*}"
|
||||
|
||||
echo "$dep_name"
|
||||
}
|
||||
|
||||
# Get the executable name from the specified `Info.plist` file
|
||||
function get_plist_executable() {
|
||||
local plist_file="$1"
|
||||
|
||||
if [ ! -f "$plist_file" ]; then
|
||||
echo ""
|
||||
else
|
||||
/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist_file" 2>/dev/null || echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the version from the specified `Info.plist` file
|
||||
function get_plist_version() {
|
||||
local plist_file="$1"
|
||||
|
||||
if [ ! -f "$plist_file" ]; then
|
||||
echo "$UNKNOWN_VERSION"
|
||||
else
|
||||
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$plist_file" 2>/dev/null || echo "$UNKNOWN_VERSION"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the path of the specified framework version
|
||||
function get_framework_path() {
|
||||
local path="$1"
|
||||
local version_path="$2"
|
||||
|
||||
if [ -z "$version_path" ]; then
|
||||
echo "$path"
|
||||
else
|
||||
echo "$path/$version_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Search for privacy manifest files in the specified directory
|
||||
function search_privacy_manifest_files() {
|
||||
local path="$1"
|
||||
local -a privacy_manifest_files=()
|
||||
|
||||
# Create a temporary file to store search results
|
||||
local temp_file="$(mktemp)"
|
||||
|
||||
# Ensure the temporary file is deleted on script exit
|
||||
trap "rm -f $temp_file" EXIT
|
||||
|
||||
# Find privacy manifest files within the specified directory and store the results in the temporary file
|
||||
find "$path" -type f -name "$PRIVACY_MANIFEST_FILE_NAME" -print0 2>/dev/null > "$temp_file"
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
privacy_manifest_files+=($(encode_path "$file"))
|
||||
done < "$temp_file"
|
||||
|
||||
echo "${privacy_manifest_files[@]}"
|
||||
}
|
||||
|
||||
# Get the privacy manifest file with the shortest path
|
||||
function get_privacy_manifest_file() {
|
||||
local privacy_manifest_file="$(printf "%s\n" "$@" | awk '{print length, $0}' | sort -n | head -n1 | cut -d ' ' -f2-)"
|
||||
|
||||
echo "$(decode_path "$privacy_manifest_file")"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2024, crasowas.
|
||||
#
|
||||
# Use of this source code is governed by a MIT-style license
|
||||
# that can be found in the LICENSE file or at
|
||||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
require 'xcodeproj'
|
||||
|
||||
RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest'
|
||||
|
||||
if ARGV.length < 2
|
||||
puts "Usage: ruby xcode_install_helper.rb <project_path> <script_content> [install_builds_only (true|false)]"
|
||||
exit 1
|
||||
end
|
||||
|
||||
project_path = ARGV[0]
|
||||
run_script_content = ARGV[1]
|
||||
install_builds_only = ARGV[2] == 'true'
|
||||
|
||||
# Find the first .xcodeproj file in the project directory
|
||||
xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first
|
||||
|
||||
# Validate the .xcodeproj file existence
|
||||
unless xcodeproj_path
|
||||
puts "Error: No .xcodeproj file found in the specified directory."
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Open the Xcode project file
|
||||
begin
|
||||
project = Xcodeproj::Project.open(xcodeproj_path)
|
||||
rescue StandardError => e
|
||||
puts "Error: Unable to open the project file - #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Process all targets in the project
|
||||
project.targets.each do |target|
|
||||
# Skip PBXAggregateTarget
|
||||
if target.is_a?(Xcodeproj::Project::Object::PBXAggregateTarget)
|
||||
puts "Skipping aggregate target: #{target.name}."
|
||||
next
|
||||
end
|
||||
|
||||
# Check if the target is a native application target
|
||||
if target.product_type == 'com.apple.product-type.application'
|
||||
puts "Processing target: #{target.name}..."
|
||||
|
||||
# Check for an existing Run Script phase with the specified name
|
||||
existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME }
|
||||
|
||||
# Remove the existing Run Script phase if found
|
||||
if existing_phase
|
||||
puts " - Removing existing Run Script."
|
||||
target.build_phases.delete(existing_phase)
|
||||
end
|
||||
|
||||
# Add the new Run Script phase at the end
|
||||
puts " - Adding new Run Script."
|
||||
new_phase = target.new_shell_script_build_phase(RUN_SCRIPT_PHASE_NAME)
|
||||
new_phase.shell_script = run_script_content
|
||||
# Disable showing environment variables in the build log
|
||||
new_phase.show_env_vars_in_log = '0'
|
||||
# Run only for deployment post-processing if install_builds_only is true
|
||||
new_phase.run_only_for_deployment_postprocessing = install_builds_only ? '1' : '0'
|
||||
# Disable dependency analysis to force the script to run on every build, unless restricted to deployment builds by post-processing setting
|
||||
new_phase.always_out_of_date = '1'
|
||||
else
|
||||
puts "Skipping non-application target: #{target.name}."
|
||||
end
|
||||
end
|
||||
|
||||
# Save the project file
|
||||
begin
|
||||
project.save
|
||||
puts "Successfully added the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'."
|
||||
rescue StandardError => e
|
||||
puts "Error: Unable to save the project file - #{e.message}"
|
||||
exit 1
|
||||
end
|
||||