Compare commits

...

125 Commits

Author SHA1 Message Date
Jose Olarte III
3544d7278d Optimized item actions
- Edited button labels for brevity
- Repositioned Totals toggle
- Restyled note about recent hours
- Various text size and spacing changes
2025-06-10 19:54:05 +08:00
Jose Olarte III
d3110506ea Optimized per-item layout
- Stacked contact name and DID
- Text truncates to leave room for action buttons when visible
- Separated "from / to" heading from buttons to minimize width
- Various spacing and alignment adjustments
2025-06-10 18:42:49 +08:00
8609f8458d bump to build 25 & version 0.5.0 2025-06-09 09:26:21 -06:00
8f5c34bc5f fix linting 2025-06-09 09:09:54 -06:00
b0d61b95ea Merge branch 'ui-fixes-2025-06-w2' 2025-06-09 08:44:42 -06:00
af7bd236a3 fix check for successful gift submission 2025-06-09 08:41:47 -06:00
d719338bcc fix problem setting 'loading' flag 2025-06-09 08:37:42 -06:00
6ddf2d1012 fix problem switching IDs (creating too many settings) 2025-06-09 08:33:33 -06:00
Jose Olarte III
1b2d4b623a Turned off automatic safe area in iOS
- Safe area implementations will solely depend on CSS styles for full control
- Eliminates doubled top and bottom padding in iOS
2025-06-09 20:20:17 +08:00
Jose Olarte III
16d5c917d2 Updated QR scanner call
- Searched for all other (outdated) calls to QR scanner dialog and updated them
- Fixed vite HTML spec warning
2025-06-09 19:36:06 +08:00
5976a4995e fix problem clicking on offer-delivery, plus some other hardening and phrasing 2025-06-08 20:13:32 -06:00
dcd0cc4c20 fix import for derived accounts and hopefully make other account-access code more robust 2025-06-08 19:14:41 -06:00
b3ca6c9d91 remove relative URL references in different target because mobile chokes 2025-06-08 16:55:03 -06:00
e9d800f601 fix a web test (all passing now) 2025-06-07 21:41:43 -06:00
b939a5e592 bump build to 23 and version to 0.4.8 2025-06-07 18:54:56 -06:00
aa62037fae bump to build 22 version 0.4.7 (though I think the android capacitor.config.json appId is wrong) 2025-06-07 18:45:24 -06:00
722020ea86 fix linting 2025-06-07 18:09:04 -06:00
96aa3f4a54 add Python dependency for electron on Mac 2025-06-07 17:54:31 -06:00
c0c5f9842b fix some errors and correct recent type duplications & bloat (cherry-picked from d8f2587d1c) 2025-06-07 17:53:36 -06:00
be27ca1855 fix more logic for tests (cherry-picked from 83acb028c7) 2025-06-07 17:42:06 -06:00
92e4570672 fix some incorrect logic & things AI hallucinated 2025-06-07 17:39:10 -06:00
820ae727ed fix linting 2025-06-07 17:19:01 -06:00
dbeb1c6b4b Merge branch 'sql-absurd-sql-back' 2025-06-07 17:18:10 -06:00
573e4b206a Merge branch 'search-map-fix' 2025-06-07 16:43:49 -06:00
abc05d426e update messaging for unknown icons on home feed 2025-06-07 16:23:07 -06:00
2ea7479d75 fix verbiage and fix the contact actions to be on the right-hand side 2025-06-07 16:03:47 -06:00
9ac9713172 fix linting 2025-06-07 15:40:52 -06:00
41dad3254d fix a non-existent description, move the description right below the image 2025-06-07 15:33:29 -06:00
485eac59a0 remove unnecessary data element from export 2025-06-07 14:02:22 -06:00
Matthew Raymer
73fc32b75d fix(import): ensure contact import works for both Dexie and absurd-sql backends
- Refactor importContacts to handle both Dexie and absurd-sql (SQLite) storage
- Add ContactDbRecord interface with all string fields strictly typed (never null)
- Add helper functions to coerce null/undefined to empty string for all string fields
- Guarantee contactMethods is always stored as a JSON string (never null)
- Add runtime validation for required fields (e.g., did)
- Ensure imported/updated contacts are type-safe and compatible with both backends
- Improve code documentation and maintainability

Security:
- No sensitive data exposed
- All fields validated and sanitized before database write
- Consistent data structure across storage backends

Testing:
- Import tested with both Dexie and absurd-sql backends
- Null/undefined fields correctly handled and coerced
- No linter/type errors remain
2025-06-07 06:01:17 +00:00
Matthew Raymer
3d8e40e92b feat(export): Replace CSV export with standardized JSON format
- Add contactsToExportJson utility function for standardized data export
- Replace CSV export with JSON format in DataExportSection
- Update file extension and MIME type to application/json
- Remove Dexie-specific export logic in favor of unified SQLite/Dexie approach
- Update success notifications to reflect JSON format
- Add TypeScript interfaces for export data structure

This change improves data portability and standardization by:
- Using a consistent JSON format for data export/import
- Supporting both SQLite and Dexie databases
- Including all contact fields in export
- Properly handling contactMethods as stringified JSON
- Maintaining backward compatibility with existing import tools

Security: No sensitive data exposure, maintains existing access controls
2025-06-07 05:02:33 +00:00
38e67f3533 update a DB save to match others, ie. first SQL then maybe Dexie 2025-06-06 19:50:16 -06:00
7f63ee7c80 add another way to fix the privacy policy manifest for third parties like GoogleToolboxForMac 2025-06-06 19:45:41 -06:00
6a47f0d3e7 format total numbers better 2025-06-06 19:40:53 -06:00
fc50a9d4c6 fix problem finding offer identifiers 2025-06-06 19:06:29 -06:00
Jose Olarte III
45f43ff363 Updated icon and splash assets 2025-06-06 18:15:42 +08:00
Jose Olarte III
7b1d4c4849 Adjusted iOS-specific paddings
- Switched to CSS max() for proper conditional padding when dealing with screens that have a notch, dynamic island, gesture bar, etc.
- Top padding should now appear more compact in iOS
2025-06-06 18:14:56 +08:00
Matthew Raymer
c1f2c3951a feat(db): improve settings retrieval resilience and logging
Enhance retrieveSettingsForActiveAccount with better error handling and logging
while maintaining core functionality. Changes focus on making the system more
debuggable and resilient without overcomplicating the logic.

Key improvements:
- Add structured error handling with specific try-catch blocks
- Implement detailed logging with [databaseUtil] prefix for easy filtering
- Add graceful fallbacks for searchBoxes parsing and missing settings
- Improve error recovery paths with safe defaults
- Maintain existing security model and data integrity

Security:
- No sensitive data in logs
- Safe JSON parsing with fallbacks
- Proper error boundaries
- Consistent state management
- Clear fallback paths

Testing:
- Verify settings retrieval works with/without active DID
- Check error handling for invalid searchBoxes
- Confirm logging provides clear debugging context
- Validate fallback to default settings works
2025-06-06 09:22:35 +00:00
9d4f726c31 bump to build # 19 version 0.4.7 for mobile packages 2025-06-05 20:30:27 -06:00
1d7f626645 fix SQL references to bad "key" -> "id" 2025-06-05 20:11:32 -06:00
c5228ba7ec fix retrieval of column names -- so now most ops are working (but not all, eg. set name) 2025-06-05 20:06:32 -06:00
6e1fcd8dee remove unused DB methods (for now) 2025-06-05 20:00:51 -06:00
5bb563d694 fix extraction of values from SQLite queries 2025-06-05 19:57:59 -06:00
a3951c9d66 refactor for clarity (no logic changes) 2025-06-05 18:30:25 -06:00
aa177a9b8c fix extraction of migration names for SQLite via Capacitor 2025-06-05 18:13:33 -06:00
03cb4720b8 fix Capacitor to use the same migrations (migrations run but accounts aren't created) 2025-06-04 22:01:14 -06:00
Jose Olarte III
0e65431f43 Map z-index fix + adjustments
- Set map z-index lower than nav
- Relocated search box to account for conditional visibility
- Various conditional fixes
- Spacing adjustments
2025-06-04 18:41:49 +08:00
297c5a2dbb disable SQLite in Java & Swift (since they don't compile) & add SQL queueing on startup
At this point, the app compiles and runs in Android & iOS but DB operations fail.
2025-06-03 19:59:28 -06:00
Jose Olarte III
92b9c9334c Clickable person & project icons
- Known entities get routed to their corresponding detail views
- Unknown entities pop up a notification
2025-06-02 21:35:15 +08:00
Jose Olarte III
706182ca0c Icon for hidden DID entity
- Display EntityIcon for known entities, eye-slash icon for hidden entities, and the person-question icon for unknown entities
- Design tweaks (spacings, mostly)
2025-06-02 17:44:37 +08:00
Matthew Raymer
68e0fc4976 merge(master): big merge for qrcode-reboot 2025-06-02 03:57:00 +00:00
504056eb90 add some time to test 30 (but shrink the per-loop timeout) 2025-06-01 15:08:32 -06:00
5a1007c49c add iOS development team ID 2025-06-01 14:29:32 -06:00
Jose Olarte III
cbc14e21ec Look in .own.did for DID, as well 2025-05-30 17:34:50 +08:00
ef3bfcdbd2 fix linting 2025-05-28 20:30:00 -06:00
ec1f27bab1 fix more logging cleanup errors 2025-05-28 20:20:09 -06:00
01c33069c4 fix more of the logging & log display 2025-05-28 20:08:09 -06:00
c637d39dc9 fix log cleanup check to actually pay attention to limit 2025-05-28 19:44:16 -06:00
3e90bafbd1 correct & simplify the DB logging 2025-05-28 19:37:01 -06:00
Matthew Raymer
d2c3e5db05 fix: one lint that got past me 2025-05-28 14:03:21 +00:00
Matthew Raymer
e824fcce2e fix: linting issues 2025-05-28 14:02:02 +00:00
Matthew Raymer
f2c49872a6 fix: resolve TypeScript errors in database service implementation
- Remove unnecessary generic type parameter from AbsurdSqlDatabaseService
- Fix type handling in operation queue and result processing
- Correct WebPlatformService dbGetOneRow implementation to use imported databaseService
- Add proper type annotations for database operation results

This commit improves type safety and fixes several TypeScript errors that were
preventing proper type checking in the database service layer.
2025-05-28 13:20:01 +00:00
Matthew Raymer
229d9184b2 WIP: BROKEN FOR ELECTRON: Fixes in progress 2025-05-28 13:09:51 +00:00
Matthew Raymer
29908b77e3 feat(types): add comprehensive type definitions for @jlongster/sql.js
- Add FileSystem and FileStream interfaces for filesystem operations

- Update Database interface with proper Promise-based return types

- Add QueryExecResult interface for structured query results

- Include FS and register_for_idb in initialization result

- Fix Database constructor to support path and options parameters

- Add proper JSDoc documentation with author and description

This change resolves TypeScript compilation errors in AbsurdSqlDatabaseService by providing complete type coverage for the SQL.js WASM module with filesystem support.
2025-05-28 11:16:56 +00:00
Jose Olarte III
3e02b3924a Look for DID in .iss field instead of .own.did 2025-05-28 19:08:15 +08:00
Matthew Raymer
16cad04e5c WIP: fix(AbsurdSqlDatabaseService) fixes to typing and other curious beasts 2025-05-28 10:56:27 +00:00
Matthew Raymer
e4f859a116 fix: update offerGiverDid to use credentialSubject.offeredBy
The offerGiverDid function was looking for offeredBy at the root level of the
OfferVerifiableCredential, but it was moved to credentialSubject in our interface
changes. This fix updates the function to look in the correct location while
maintaining the same fallback behavior of using the issuer if offeredBy is not
available.

- Update path from claim.offeredBy to claim.credentialSubject.offeredBy
- Remove unnecessary string type cast
- Keep issuer fallback behavior unchanged
2025-05-28 10:41:39 +00:00
Matthew Raymer
7f17a3d9c7 refactor: remove unused imports and parameters in passkeyDidPeer.ts
- Remove unused imports:
  - DIDResolutionResult from did-resolver
  - sha256 from ethereum-cryptography/sha256.js
- Remove unused parameters from verifyJwtP256 and verifyJwtWebCrypto:
  - credIdHex
  - clientDataJsonBase64Url
  - credId

This change improves code cleanliness by removing unused code while
maintaining the core passkey authentication functionality.
2025-05-28 10:37:01 +00:00
Matthew Raymer
2d4d9691ca fix: use challenge parameter in verifyJwtWebCrypto preimage
- Remove unused client data hashing in verifyJwtWebCrypto
- Use challenge parameter directly in preimage construction
- Fix TS6133 error for unused challenge parameter
- Make verification logic consistent with verifyJwtP256

This change maintains the same verification logic while properly
utilizing the challenge parameter in the signature verification.
2025-05-28 10:28:57 +00:00
Matthew Raymer
63575b36ed fix: use challenge parameter in verifyJwtP256 preimage
- Remove unused client data hashing in verifyJwtP256
- Use challenge parameter directly in preimage construction
- Fix TS6133 error for unused challenge parameter

This change maintains the same verification logic while properly
utilizing the challenge parameter in the signature verification.
2025-05-28 10:27:19 +00:00
Matthew Raymer
2eb46367bc fix: resolve TypeScript errors in passkeyDidPeer.ts
- Fix import path for VerifyAuthenticationResponseOpts to use main package
- Add proper type assertions for credential response using AuthenticatorAssertionResponse
- Add back p256 import from @noble/curves/p256
- Remove unused functions marked with @typescript-eslint/no-unused-vars:
  - peerDidToDidDocument
  - COSEtoPEM
  - base64urlDecodeArrayBuffer
  - base64urlEncodeArrayBuffer
  - pemToCryptoKey

This change improves type safety and removes dead code while maintaining
the core passkey authentication functionality.
2025-05-28 10:24:17 +00:00
Matthew Raymer
cea0456148 fix: resolve type conflicts in AccountKeyInfo and KeyMeta imports
- Update AccountKeyInfo interface to handle derivationPath type conflict
- Fix circular dependency by importing KeyMeta directly from interfaces/common
- Use Omit utility type to properly merge Account and KeyMeta types
- Make derivationPath optional in AccountKeyInfo to match Account type

This change resolves type compatibility issues while maintaining
the intended functionality of account metadata handling.
2025-05-28 10:17:20 +00:00
Matthew Raymer
6f5db13a49 fix: consolidate KeyMeta interface and improve type safety
- Remove duplicate KeyMeta interface from crypto/vc/index.ts
- Import KeyMeta from common.ts as single source of truth
- Add missing fields to KeyMeta interface (identity, passkeyCredIdHex)
- Remove unused ErrorResponse interface from endorserServer.ts
- Fix import path for KeyMeta in crypto/vc/index.ts

This change resolves type compatibility issues and ensures consistent
KeyMeta type usage across the codebase.
2025-05-28 10:13:01 +00:00
Matthew Raymer
068662625d fix: resolve type compatibility in offerGiverDid
- Update offerGiverDid to use GenericVerifiableCredential as base type
- Add type assertion for OfferVerifiableCredential inside function
- Remove unnecessary type assertion in canFulfillOffer
2025-05-28 10:09:13 +00:00
Matthew Raymer
23627835f9 refactor: improve type safety in endorser server and common interfaces
- Add proper type definitions for AxiosErrorResponse with detailed error structure
- Make KeyMeta fields required where needed (publicKeyHex, mnemonic, derivationPath)
- Add QuantitativeValue type for consistent handling of numeric values
- Fix type assertions and compatibility between GenericVerifiableCredential and its extensions
- Improve error handling with proper type guards and assertions
- Update VerifiableCredentialClaim interface with required fields
- Add proper type assertions for claim objects in claimSummary and claimSpecialDescription
- Fix BLANK_GENERIC_SERVER_RECORD to include required @context field

Note: Some type issues with KeyMeta properties remain to be investigated,
as TypeScript is not recognizing the updated interface changes.
2025-05-28 09:29:19 +00:00
Matthew Raymer
f1ba6f9231 fix(endorserServer): improve type safety in claim handling
- Fix type conversion in claimSpecialDescription to properly handle GenericVerifiableCredential
- Update claim type checking to use 'claim' in claim for proper type narrowing
- Add type assertions for claim.object to ensure type safety
- Remove incorrect GenericCredWrapper type cast

This change resolves the type conversion error by properly handling
different claim types and ensuring type safety when passing objects
to claimSummary. The code now correctly distinguishes between
GenericVerifiableCredential and GenericCredWrapper types.
2025-05-28 09:04:15 +00:00
Matthew Raymer
137fce3e30 fix(endorserServer): improve type safety and error handling
- Update claimSummary to handle both GenericVerifiableCredential and GenericCredWrapper types
- Fix logger error handling in register function to properly stringify response data
- Add type narrowing with 'claim' in claim check for safer type handling
- Improve error message formatting for registration errors

This change improves type safety by properly handling different claim types
and ensures consistent error logging. The registration error handling now
properly stringifies response data for better debugging.
2025-05-28 09:00:46 +00:00
Matthew Raymer
7166dadbc0 fix(types): correct type imports and improve null safety
- Move type imports to their correct source locations:
  - GenericCredWrapper, GenericVerifiableCredential from interfaces/common
  - GiveSummaryRecord from interfaces/records
  - OfferVerifiableCredential from interfaces/claims
- Add null safety check for dbResult in retrieveAccountCount
- Initialize result variable to prevent undefined access

This change fixes TypeScript errors by ensuring types are imported from their
proper source files and improves code safety by adding proper null checks.
The type system can now correctly validate the usage of these types across
the codebase.
2025-05-28 08:56:54 +00:00
Matthew Raymer
bc274bdf7f fix(types): improve account type safety and metadata handling
- Change retrieveAllAccountsMetadata to return Account[] instead of AccountEncrypted[]
  to better reflect its purpose of returning non-sensitive metadata
- Update ImportDerivedAccountView to use Account type and group by derivation path
- Update retrieveAllFullyDecryptedAccounts to use AccountEncrypted type for encrypted data
- Fix import path for Account type in ImportDerivedAccountView

This change improves type safety by making it explicit which functions handle
encrypted data vs metadata, and ensures consistent handling of account data
across the application. The metadata functions now correctly strip sensitive
fields while functions that need encrypted data maintain access to those fields.
2025-05-28 08:52:09 +00:00
Matthew Raymer
082f8c0126 feat(QRScanner): implement QRScannerOptions in WebInlineQRScanner
- Add proper handling of QRScannerOptions in startScan method
- Implement camera selection (front/back) via options.camera
- Add video preview toggle via options.showPreview
- Store options as class property for persistence
- Improve logging with options context
- Fix TypeScript error for unused options parameter

This change makes the QR scanner more configurable and properly
implements the QRScannerService interface contract.
2025-05-28 08:43:43 +00:00
Matthew Raymer
fd09c7e426 fix(deepLinks): improve route validation and type safety
- Add early validation for route paths to prevent undefined access
- Introduce INVALID_ROUTE error type with detailed error information
- Simplify parameter mapping using nullish coalescing operator
- Improve type safety in parseDeepLink method
- Add better error details for invalid route paths

This change prevents potential runtime errors from undefined route access
and provides clearer error messages for invalid deep links.
2025-05-28 08:40:49 +00:00
Matthew Raymer
be40643379 fix: unused element 2025-05-28 08:38:10 +00:00
Matthew Raymer
835a270e65 feat(qr-scanner): Add camera state management to CapacitorQRScanner
- Add camera state tracking and listener management
- Implement addCameraStateListener and removeCameraStateListener methods
- Add state transitions during scanning operations
- Improve error handling with state updates
- Add proper type imports for CameraState and CameraStateListener

This change ensures CapacitorQRScanner fully implements the QRScannerService
interface and provides proper camera state feedback to consumers. Camera state
is now tracked through the entire lifecycle of scanning operations, with
appropriate state transitions for initialization, active scanning, errors,
and cleanup.
2025-05-28 08:37:02 +00:00
Jose Olarte III
8b03789941 Change heading based on crop flag 2025-05-28 16:32:41 +08:00
Jose Olarte III
b4a6b99301 Better error handling for image upload 2025-05-28 16:17:49 +08:00
Matthew Raymer
13682a1930 fix(db): add type declarations for SQL.js and absurd-sql modules
- Create type declarations in interfaces/absurd-sql.d.ts
- Import and use proper QueryExecResult and SqlValue types
- Add declarations for all required modules:
  - @jlongster/sql.js
  - absurd-sql
  - absurd-sql/dist/indexeddb-backend
  - absurd-sql/dist/indexeddb-main-thread
- Ensure type safety for database operations

This change resolves TypeScript errors about missing type declarations
while maintaining proper type safety for database operations. The
declarations are placed in the interfaces folder to match the project's
type organization.
2025-05-28 06:21:20 +00:00
Matthew Raymer
669a66c24c fix(db): improve type safety in AbsurdSqlDatabaseService
- Move external module type declarations to dedicated .d.ts file
- Make SQL.js types compatible with our database interface
- Fix type compatibility between operation queue and database results
- Add proper typing for database operations and results

This change improves type safety by:
1. Properly declaring types for external modules
2. Ensuring database operation results match our interface
3. Making the operation queue type-safe with generics
4. Removing duplicate type definitions

The remaining module resolution warnings can be safely ignored as they
don't affect runtime behavior and our type declarations are working.
2025-05-28 06:07:50 +00:00
Matthew Raymer
13505b539e fix(db): resolve type compatibility in AbsurdSqlDatabaseService
- Make QueuedOperation interface generic to properly handle operation types
- Fix type compatibility between queue operations and their resolvers
- Use explicit typing for operation queue to handle generic types
- Maintain type safety while allowing different operation return types

This fixes a TypeScript error where the operation queue's resolve function
was not properly typed to handle generic return types from database operations.
2025-05-28 06:02:55 +00:00
Matthew Raymer
07ac340733 feat(capacitor): implement storage permission checks for file operations
- Add permission checks before writeFile and writeAndShareFile operations
- Reuse existing checkStoragePermissions method for Android devices
- Maintain iOS-specific handling (early return for iOS permission model)
- Improve error handling and logging for permission-related issues

This change ensures proper storage permission handling on Android devices
while maintaining the existing iOS behavior. The permission checks run
before any file write operations, providing better error handling and
user experience.
2025-05-28 05:56:58 +00:00
Matthew Raymer
ba2b2fc543 fix: placeholder for PyWebViewPlatformService writeAndShareFile 2025-05-28 05:06:31 +00:00
21184e7625 fix spelling of SQLite module for iOS 2025-05-27 21:11:07 -06:00
8d1511e38f convert all remaining DB writes & reads to SQL (with successful registration & claim) 2025-05-27 21:07:24 -06:00
Matthew Raymer
b18112b869 WIP: disabling absurd-sql when using Capacitor SQLite 2025-05-27 13:15:41 +00:00
Matthew Raymer
a228a9b1c0 fix: add requirements for capacitor/sqlite 2025-05-27 12:42:27 +00:00
Matthew Raymer
1560ff0829 feature: fleshed out capacitor and electron database operators 2025-05-27 11:23:52 +00:00
Jose Olarte III
e839997f91 TEST: platform- and camera-specific mirroring 2025-05-27 18:58:35 +08:00
Jose Olarte III
d8d054a0e1 Streamlined QR scanner web camera
- No need to stop and start camera preview
2025-05-27 18:57:12 +08:00
Jose Olarte III
efc720e47f Mobile native to use web camera
- Ensure consistent UI experience for uploading photos across mobile web and native
2025-05-27 17:46:19 +08:00
Jose Olarte III
0a85bea533 Feature: context-based default camera
- Specify the default camera (front / back) to use
2025-05-27 15:37:45 +08:00
7de4125eb7 add SQL DB access to everywhere we are using the DB, up to the "C" files 2025-05-27 01:27:04 -06:00
Matthew Raymer
81d4f0c762 fix: resolve PWA build issues with SQL.js worker files
- Update worker format to ESM in Vite config to fix IIFE format error

- Increase PWA precache file size limit to 10MB to accommodate SQL.js files

- Fix type declarations for worker configuration

- Add proper type annotations for Vite config

- Add type declarations for absurd-sql module
2025-05-27 06:54:29 +00:00
4c1b4fe651 fix linting 2025-05-26 22:56:00 -06:00
Matthew Raymer
e63541ef53 fix: update ESLint and VS Code settings
- Configure ESLint to ignore node_modules
- Add VS Code settings for Java diagnostics

This fixes the Android build issues and improves the development
environment by properly ignoring node_modules in linting and
diagnostics.
2025-05-27 04:43:52 +00:00
0bfc18c385 add encryption & decryption for the sensitive identity & mnemonic in SQL DB 2025-05-26 22:42:20 -06:00
Matthew Raymer
35f5df6b6b fix: move lexical declarations outside case blocks
fix: missing logger import
Move variable declarations outside switch statement in AbsurdSqlDatabaseService
to fix 'no-case-declarations' linter errors.
2025-05-27 03:21:55 +00:00
Matthew Raymer
0f1ac2b230 fix: move lexical declarations outside case blocks in AbsurdSqlDatabaseService
- Move queryResult and allResult declarations outside switch statement
- Change const declarations to let since they're now in outer scope
- Remove const declarations from inside case blocks

This fixes the 'no-case-declarations' linter errors by ensuring variables
are declared in a scope that encompasses all case blocks, preventing
potential scoping issues.

Note: Type definition errors for external modules remain and should be
addressed separately.
2025-05-27 03:14:02 +00:00
3c0bdeaed3 remove debugging console statements 2025-05-26 20:43:06 -06:00
11f2527b04 start adding the SQL approach to files, also using the Dexie approach if desired 2025-05-26 20:26:28 -06:00
5d8175aeeb add encryption for the two SQL columns, replace basic DB utils, add USE_DEXIE_DB flag, and start adding SQL everywhere 2025-05-26 19:03:20 -06:00
b6b95cb0d0 remove unused redirect to start page (now that we're creating an ID up front) 2025-05-26 16:29:10 -06:00
655c5188a4 add queueing to the DB service so that requests get processed in order 2025-05-26 16:28:33 -06:00
8b7451330f remove possibility of failing a migration script and then succeeding on later ones 2025-05-26 15:50:37 -06:00
b8fbc3f7a6 fix console error about "window" unavailable due to service worker 2025-05-26 15:49:44 -06:00
92dadba1cb rename the absurd-sql-specific items for clarity 2025-05-26 14:52:39 -06:00
3a6f585de0 adjust so DB calls go to the factory 2025-05-26 13:59:34 -06:00
Jose Olarte III
47501ae917 Linting 2025-05-26 19:23:41 +08:00
Jose Olarte III
28634839ec Feature: front/back camera toggle
- Added to gifting and profile dialog camera for now. Toggle button is hidden in desktop.
- WIP: same feature for QR scanner camera.
- WIP: ability to specify default camera depending on where it's called.
2025-05-26 19:23:28 +08:00
1b7c96ed9b don't highlight profile Advanced link in blue 2025-05-13 19:37:47 -06:00
41365fab8f add projectLink to onboarding meeting, plus enhancements to setup usability 2025-05-13 19:36:23 -06:00
5cc42be58a fix some test scripts 2025-04-08 20:31:47 -06:00
3d1a2eeb8d adjust to app.timesafari.app in more places 2025-04-08 20:29:08 -06:00
7b0ee2e44e more ios folders to ignore (until we figure out the right way to dance with capacitor-assets) 2025-04-06 19:52:44 -06:00
ac018997e8 adjust instructions for capacitor-assets and more files 2025-04-06 19:48:45 -06:00
6f449e9c1f restore important file from previous cleanup 2025-04-06 19:15:00 -06:00
543599a6a1 remove icon files that are generated by capacitor-assets 2025-04-06 19:02:01 -06:00
189 changed files with 9186 additions and 4044 deletions

View File

@@ -2,11 +2,12 @@
# iOS doesn't like spaces in the app title. # iOS doesn't like spaces in the app title.
TIME_SAFARI_APP_TITLE="TimeSafari_Dev" 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). # 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_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
# Using shared server by default to ease setup, which works for shared test users. # 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_IMAGE_API_SERVER=https://test-image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
VITE_PASSKEYS_ENABLED=true VITE_PASSKEYS_ENABLED=true

View File

@@ -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

View File

@@ -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_IMAGE_API_SERVER=https://image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app

View File

@@ -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_IMAGE_API_SERVER=https://test-image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
VITE_PASSKEYS_ENABLED=true VITE_PASSKEYS_ENABLED=true

View File

@@ -4,6 +4,12 @@ module.exports = {
node: true, node: true,
es2022: true, es2022: true,
}, },
ignorePatterns: [
'node_modules/',
'dist/',
'dist-electron/',
'*.d.ts'
],
extends: [ extends: [
"plugin:vue/vue3-recommended", "plugin:vue/vue3-recommended",
"eslint:recommended", "eslint:recommended",

5
.gitignore vendored
View File

@@ -51,6 +51,7 @@ vendor/
# Build logs # Build logs
build_logs/ build_logs/
android/app/src/main/assets/public # PWA icon files generated by capacitor-assets
android/app/src/main/res icons

View File

@@ -9,19 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended) - Node.js (LTS version recommended)
- npm (comes with Node.js) - npm (comes with Node.js)
- Git - Git
- For Android builds: Android Studio with SDK installed
- For iOS builds: macOS with Xcode and ruby gems & bundle
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
- For desktop builds: Additional build tools based on your OS - For desktop builds: Additional build tools based on your OS
## Forks ## Forks
@@ -84,7 +71,7 @@ Install dependencies:
* For test, build the app (because test server is not yet set up to build): * For test, build the app (because test server is not yet set up to build):
```bash ```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 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
``` ```
... and transfer to the test server: ... and transfer to the test server:
@@ -326,6 +313,32 @@ npm run build:electron-prod && npm run electron:start
Prerequisites: macOS with Xcode installed Prerequisites: macOS with Xcode installed
#### 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 XCode dependencies change):
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
```bash
cd ios/App
pod install
```
1. Build the web assets: 1. Build the web assets:
```bash ```bash
@@ -334,6 +347,7 @@ Prerequisites: macOS with Xcode installed
npm run build:capacitor npm run build:capacitor
``` ```
2. Update iOS project with latest build: 2. Update iOS project with latest build:
```bash ```bash
@@ -345,7 +359,11 @@ Prerequisites: macOS with Xcode installed
3. Copy the assets: 3. Copy the assets:
```bash ```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 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 npx capacitor-assets generate --ios
``` ```
@@ -353,10 +371,10 @@ Prerequisites: macOS with Xcode installed
``` ```
cd ios/App cd ios/App
xcrun agvtool new-version 15 xcrun agvtool new-version 25
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.5;/g" > temp cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.0;/g" > temp
mv temp App.xcodeproj/project.pbxproj mv temp App.xcodeproj/project.pbxproj
cd - cd -
``` ```
@@ -369,28 +387,25 @@ Prerequisites: macOS with Xcode installed
6. 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 7. Release
* Under "General" renamed a bunch of things to "Time Safari" * Someday: Under "General" we want to rename a bunch of things to "Time Safari"
* Choose Product -> Destination -> Build Any iOS * Choose Product -> Destination -> Any iOS Device
* Choose Product -> Archive * Choose Product -> Archive
* This will trigger a build and take time, needing user's "login" keychain password which is just their login password, repeatedly. * 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`). * 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 * 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. * 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. * 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. * You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review". * Then "Save" and "Add to Review" and "Resubmit to App Review".
#### First-time iOS Configuration
- Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team.
### Android Build ### Android Build
Prerequisites: Android Studio with SDK installed Prerequisites: Android Studio with Java SDK installed
1. Build the web assets: 1. Build the web assets:
@@ -445,7 +460,9 @@ Prerequisites: Android Studio with SDK installed
* Then `bundleRelease`: * Then `bundleRelease`:
```bash ```bash
cd android
./gradlew bundleRelease -Dlint.baselines.continue=true ./gradlew bundleRelease -Dlint.baselines.continue=true
cd -
``` ```
... and find your `aab` file at app/build/outputs/bundle/release ... and find your `aab` file at app/build/outputs/bundle/release
@@ -458,6 +475,8 @@ At play.google.com/console:
- Hit "Next". - Hit "Next".
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review". - 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.
## First-time Android Configuration for deep links ## First-time Android Configuration for deep links

View File

@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.4.7]
### Fixed
- Cameras everywhere
### Changed
- IndexedDB -> SQLite
## [0.4.5] - 2025.02.23 ## [0.4.5] - 2025.02.23
### Added ### Added
- Total amounts of gives on project page - Total amounts of gives on project page

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 18 versionCode 25
versionName "0.4.7" versionName "0.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -91,6 +91,8 @@ dependencies {
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android') implementation project(':capacitor-android')
implementation project(':capacitor-community-sqlite')
implementation "androidx.biometric:biometric:1.2.0-alpha05"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

View File

@@ -9,6 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies { dependencies {
implementation project(':capacitor-community-sqlite')
implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-mlkit-barcode-scanning')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-camera') implementation project(':capacitor-camera')

View File

@@ -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"
]
} }
} }

View File

@@ -1,4 +1,8 @@
[ [
{
"pkg": "@capacitor-community/sqlite",
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
},
{ {
"pkg": "@capacitor-mlkit/barcode-scanning", "pkg": "@capacitor-mlkit/barcode-scanning",
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin" "classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"

View File

@@ -1,7 +1,15 @@
package app.timesafari; package app.timesafari;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
//import com.getcapacitor.community.sqlite.SQLite;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
// ... existing code ... @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize SQLite
//registerPlugin(SQLite.class);
}
} }

View File

@@ -1,5 +0,0 @@
package timesafari.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <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> </adaptive-icon>

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <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> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -2,6 +2,9 @@
include ':capacitor-android' include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 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' include ':capacitor-mlkit-barcode-scanning'
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android') project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
assets/splash-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -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"
]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,45 +3,46 @@
## Core Services ## Core Services
### 1. Storage Service Layer ### 1. Storage Service Layer
- [ ] Create base `StorageService` interface - [x] Create base `PlatformService` interface
- [ ] Define common methods for all platforms - [x] Define common methods for all platforms
- [ ] Add platform-specific method signatures - [x] Add platform-specific method signatures
- [ ] Include error handling types - [x] Include error handling types
- [ ] Add migration support methods - [x] Add migration support methods
- [ ] Implement platform-specific services - [x] Implement platform-specific services
- [ ] `WebSQLiteService` (absurd-sql) - [x] `AbsurdSqlDatabaseService` (web)
- [ ] Database initialization - [x] Database initialization
- [ ] VFS setup with IndexedDB backend - [x] VFS setup with IndexedDB backend
- [ ] Connection management - [x] Connection management
- [ ] Query builder - [x] Operation queuing
- [ ] `NativeSQLiteService` (iOS/Android) - [ ] `NativeSQLiteService` (iOS/Android) (planned)
- [ ] SQLCipher integration - [ ] SQLCipher integration
- [ ] Native bridge setup - [ ] Native bridge setup
- [ ] File system access - [ ] File system access
- [ ] `ElectronSQLiteService` - [ ] `ElectronSQLiteService` (planned)
- [ ] Node SQLite integration - [ ] Node SQLite integration
- [ ] IPC communication - [ ] IPC communication
- [ ] File system access - [ ] File system access
### 2. Migration Services ### 2. Migration Services
- [ ] Implement `MigrationService` - [x] Implement basic migration support
- [ ] Backup creation - [x] Dual-storage pattern (SQLite + Dexie)
- [ ] Data verification - [x] Basic data verification
- [ ] Rollback procedures - [ ] Rollback procedures (planned)
- [ ] Progress tracking - [ ] Progress tracking (planned)
- [ ] Create `MigrationUI` components - [ ] Create `MigrationUI` components (planned)
- [ ] Progress indicators - [ ] Progress indicators
- [ ] Error handling - [ ] Error handling
- [ ] User notifications - [ ] User notifications
- [ ] Manual triggers - [ ] Manual triggers
### 3. Security Layer ### 3. Security Layer
- [ ] Implement `EncryptionService` - [x] Basic data integrity
- [ ] Implement `EncryptionService` (planned)
- [ ] Key management - [ ] Key management
- [ ] Encryption/decryption - [ ] Encryption/decryption
- [ ] Secure storage - [ ] Secure storage
- [ ] Add `BiometricService` - [ ] Add `BiometricService` (planned)
- [ ] Platform detection - [ ] Platform detection
- [ ] Authentication flow - [ ] Authentication flow
- [ ] Fallback mechanisms - [ ] Fallback mechanisms
@@ -49,18 +50,19 @@
## Platform-Specific Implementation ## Platform-Specific Implementation
### Web Platform ### Web Platform
- [ ] Setup absurd-sql - [x] Setup absurd-sql
- [ ] Install dependencies - [x] Install dependencies
```json ```json
{ {
"@jlongster/sql.js": "^1.8.0", "@jlongster/sql.js": "^1.8.0",
"absurd-sql": "^1.8.0" "absurd-sql": "^1.8.0"
} }
``` ```
- [ ] Configure VFS with IndexedDB backend - [x] Configure VFS with IndexedDB backend
- [ ] Setup worker threads - [x] Setup worker threads
- [ ] Implement connection pooling - [x] Implement operation queuing
- [ ] Configure database pragmas - [x] Configure database pragmas
```sql ```sql
PRAGMA journal_mode=MEMORY; PRAGMA journal_mode=MEMORY;
PRAGMA synchronous=NORMAL; PRAGMA synchronous=NORMAL;
@@ -68,19 +70,19 @@
PRAGMA busy_timeout=5000; PRAGMA busy_timeout=5000;
``` ```
- [ ] Update build configuration - [x] Update build configuration
- [ ] Modify `vite.config.ts` - [x] Modify `vite.config.ts`
- [ ] Add worker configuration - [x] Add worker configuration
- [ ] Update chunk splitting - [x] Update chunk splitting
- [ ] Configure asset handling - [x] Configure asset handling
- [ ] Implement IndexedDB fallback - [x] Implement IndexedDB backend
- [ ] Create fallback service - [x] Create database service
- [ ] Add data synchronization - [x] Add operation queuing
- [ ] Handle quota exceeded - [x] Handle initialization
- [ ] Implement atomic operations - [x] Implement atomic operations
### iOS Platform ### iOS Platform (Planned)
- [ ] Setup SQLCipher - [ ] Setup SQLCipher
- [ ] Install pod dependencies - [ ] Install pod dependencies
- [ ] Configure encryption - [ ] Configure encryption
@@ -93,7 +95,7 @@
- [ ] Configure backup - [ ] Configure backup
- [ ] Setup app groups - [ ] Setup app groups
### Android Platform ### Android Platform (Planned)
- [ ] Setup SQLCipher - [ ] Setup SQLCipher
- [ ] Add Gradle dependencies - [ ] Add Gradle dependencies
- [ ] Configure encryption - [ ] Configure encryption
@@ -106,7 +108,7 @@
- [ ] Configure backup - [ ] Configure backup
- [ ] Setup file provider - [ ] Setup file provider
### Electron Platform ### Electron Platform (Planned)
- [ ] Setup Node SQLite - [ ] Setup Node SQLite
- [ ] Install dependencies - [ ] Install dependencies
- [ ] Configure IPC - [ ] Configure IPC
@@ -122,7 +124,8 @@
## Data Models and Types ## Data Models and Types
### 1. Database Schema ### 1. Database Schema
- [ ] Define tables - [x] Define tables
```sql ```sql
-- Accounts table -- Accounts table
CREATE TABLE accounts ( CREATE TABLE accounts (
@@ -155,13 +158,14 @@
CREATE INDEX idx_settings_updated_at ON settings(updated_at); CREATE INDEX idx_settings_updated_at ON settings(updated_at);
``` ```
- [ ] Create indexes - [x] Create indexes
- [ ] Define constraints - [x] Define constraints
- [ ] Add triggers - [ ] Add triggers (planned)
- [ ] Setup migrations - [ ] Setup migrations (planned)
### 2. Type Definitions ### 2. Type Definitions
- [ ] Create interfaces
- [x] Create interfaces
```typescript ```typescript
interface Account { interface Account {
did: string; did: string;
@@ -185,28 +189,28 @@
} }
``` ```
- [ ] Add validation - [x] Add validation
- [ ] Create DTOs - [x] Create DTOs
- [ ] Define enums - [x] Define enums
- [ ] Add type guards - [x] Add type guards
## UI Components ## UI Components
### 1. Migration UI ### 1. Migration UI (Planned)
- [ ] Create components - [ ] Create components
- [ ] `MigrationProgress.vue` - [ ] `MigrationProgress.vue`
- [ ] `MigrationError.vue` - [ ] `MigrationError.vue`
- [ ] `MigrationSettings.vue` - [ ] `MigrationSettings.vue`
- [ ] `MigrationStatus.vue` - [ ] `MigrationStatus.vue`
### 2. Settings UI ### 2. Settings UI (Planned)
- [ ] Update components - [ ] Update components
- [ ] Add storage settings - [ ] Add storage settings
- [ ] Add migration controls - [ ] Add migration controls
- [ ] Add backup options - [ ] Add backup options
- [ ] Add security settings - [ ] Add security settings
### 3. Error Handling UI ### 3. Error Handling UI (Planned)
- [ ] Create components - [ ] Create components
- [ ] `StorageError.vue` - [ ] `StorageError.vue`
- [ ] `QuotaExceeded.vue` - [ ] `QuotaExceeded.vue`
@@ -216,20 +220,20 @@
## Testing ## Testing
### 1. Unit Tests ### 1. Unit Tests
- [ ] Test services - [x] Basic service tests
- [ ] Storage service tests - [x] Platform service tests
- [ ] Migration service tests - [x] Database operation tests
- [ ] Security service tests - [ ] Security service tests (planned)
- [ ] Platform detection tests - [ ] Platform detection tests (planned)
### 2. Integration Tests ### 2. Integration Tests (Planned)
- [ ] Test migrations - [ ] Test migrations
- [ ] Web platform tests - [ ] Web platform tests
- [ ] iOS platform tests - [ ] iOS platform tests
- [ ] Android platform tests - [ ] Android platform tests
- [ ] Electron platform tests - [ ] Electron platform tests
### 3. E2E Tests ### 3. E2E Tests (Planned)
- [ ] Test workflows - [ ] Test workflows
- [ ] Account management - [ ] Account management
- [ ] Settings management - [ ] Settings management
@@ -239,12 +243,12 @@
## Documentation ## Documentation
### 1. Technical Documentation ### 1. Technical Documentation
- [ ] Update architecture docs - [x] Update architecture docs
- [ ] Add API documentation - [x] Add API documentation
- [ ] Create migration guides - [ ] Create migration guides (planned)
- [ ] Document security measures - [ ] Document security measures (planned)
### 2. User Documentation ### 2. User Documentation (Planned)
- [ ] Update user guides - [ ] Update user guides
- [ ] Add troubleshooting guides - [ ] Add troubleshooting guides
- [ ] Create FAQ - [ ] Create FAQ
@@ -253,18 +257,18 @@
## Deployment ## Deployment
### 1. Build Process ### 1. Build Process
- [ ] Update build scripts - [x] Update build scripts
- [ ] Add platform-specific builds - [x] Add platform-specific builds
- [ ] Configure CI/CD - [ ] Configure CI/CD (planned)
- [ ] Setup automated testing - [ ] Setup automated testing (planned)
### 2. Release Process ### 2. Release Process (Planned)
- [ ] Create release checklist - [ ] Create release checklist
- [ ] Add version management - [ ] Add version management
- [ ] Setup rollback procedures - [ ] Setup rollback procedures
- [ ] Configure monitoring - [ ] Configure monitoring
## Monitoring and Analytics ## Monitoring and Analytics (Planned)
### 1. Error Tracking ### 1. Error Tracking
- [ ] Setup error logging - [ ] Setup error logging
@@ -278,7 +282,7 @@
- [ ] Monitor performance - [ ] Monitor performance
- [ ] Collect user feedback - [ ] Collect user feedback
## Security Audit ## Security Audit (Planned)
### 1. Code Review ### 1. Code Review
- [ ] Review encryption - [ ] Review encryption
@@ -295,29 +299,31 @@
## Success Criteria ## Success Criteria
### 1. Performance ### 1. Performance
- [ ] Query response time < 100ms - [x] Query response time < 100ms
- [ ] Migration time < 5s per 1000 records - [x] Operation queuing for thread safety
- [ ] Storage overhead < 10% - [x] Proper initialization handling
- [ ] Memory usage < 50MB - [ ] Migration time < 5s per 1000 records (planned)
- [ ] Atomic operations complete successfully - [ ] Storage overhead < 10% (planned)
- [ ] Transaction performance meets requirements - [ ] Memory usage < 50MB (planned)
### 2. Reliability ### 2. Reliability
- [ ] 99.9% uptime - [x] Basic data integrity
- [ ] Zero data loss - [x] Operation queuing
- [ ] Automatic recovery - [ ] Automatic recovery (planned)
- [ ] Backup verification - [ ] Backup verification (planned)
- [ ] Transaction atomicity - [ ] Transaction atomicity (planned)
- [ ] Data consistency - [ ] Data consistency (planned)
### 3. Security ### 3. Security
- [ ] AES-256 encryption - [x] Basic data integrity
- [ ] Secure key storage - [ ] AES-256 encryption (planned)
- [ ] Access control - [ ] Secure key storage (planned)
- [ ] Audit logging - [ ] Access control (planned)
- [ ] Audit logging (planned)
### 4. User Experience ### 4. User Experience
- [ ] Smooth migration - [x] Basic database operations
- [ ] Clear error messages - [ ] Smooth migration (planned)
- [ ] Progress indicators - [ ] Clear error messages (planned)
- [ ] Recovery options - [ ] Progress indicators (planned)
- [ ] Recovery options (planned)

13
ios/.gitignore vendored
View File

@@ -11,3 +11,16 @@ capacitor-cordova-ios-plugins
# Generated Config files # Generated Config files
App/App/capacitor.config.json App/App/capacitor.config.json
App/App/config.xml 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

View File

@@ -14,7 +14,7 @@
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; 97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -27,9 +27,9 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; 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>"; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; }; 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>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -37,17 +37,17 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */, 97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { 4B546315E668C7A13939F417 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -57,8 +57,8 @@
children = ( children = (
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */, BA325FFCDCE8D334E5C7AEBE /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, 4B546315E668C7A13939F417 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -85,13 +85,13 @@
path = App; path = App;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7F8756D8B27F46E3366F6CEA /* Pods */ = { BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
); );
name = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
/* End PBXGroup section */ /* End PBXGroup section */
@@ -101,12 +101,13 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = ( buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, 92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */, 504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */, 504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */, 504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
); );
buildRules = ( buildRules = (
); );
@@ -186,28 +187,10 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" "; shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
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;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -222,6 +205,47 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0; 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -375,11 +399,12 @@
}; };
504EC3171FED79650016851F /* Debug */ = { 504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
@@ -388,7 +413,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.4.7; MARKETING_VERSION = 0.5.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -401,11 +426,12 @@
}; };
504EC3181FED79650016851F /* Release */ = { 504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
@@ -414,7 +440,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.4.7; MARKETING_VERSION = 0.5.0;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -1,5 +1,6 @@
import UIKit import UIKit
import Capacitor import Capacitor
import CapacitorCommunitySqlite
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -7,6 +8,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize SQLite
//let sqlite = SQLite()
//sqlite.initialize()
// Override point for customization after application launch. // Override point for customization after application launch.
return true return true
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -1,14 +0,0 @@
{
"images": [
{
"idiom": "universal",
"size": "1024x1024",
"filename": "AppIcon-512@2x.png",
"platform": "ios"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -1,23 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "splash-2732x2732-2.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :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 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
@@ -26,4 +27,9 @@ end
post_install do |installer| post_install do |installer|
assertDeploymentTarget(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 end

View File

@@ -5,6 +5,10 @@ PODS:
- Capacitor - Capacitor
- CapacitorCamera (6.1.2): - CapacitorCamera (6.1.2):
- Capacitor - Capacitor
- CapacitorCommunitySqlite (6.0.2):
- Capacitor
- SQLCipher
- ZIPFoundation
- CapacitorCordova (6.2.1) - CapacitorCordova (6.2.1)
- CapacitorFilesystem (6.0.3): - CapacitorFilesystem (6.0.3):
- Capacitor - Capacitor
@@ -73,11 +77,18 @@ PODS:
- nanopb/decode (2.30910.0) - nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0) - nanopb/encode (2.30910.0)
- PromisesObjC (2.4.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: DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)" - "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)" - "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
@@ -98,6 +109,8 @@ SPEC REPOS:
- MLKitVision - MLKitVision
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- SQLCipher
- ZIPFoundation
EXTERNAL SOURCES: EXTERNAL SOURCES:
Capacitor: Capacitor:
@@ -106,6 +119,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/app" :path: "../../node_modules/@capacitor/app"
CapacitorCamera: CapacitorCamera:
:path: "../../node_modules/@capacitor/camera" :path: "../../node_modules/@capacitor/camera"
CapacitorCommunitySqlite:
:path: "../../node_modules/@capacitor-community/sqlite"
CapacitorCordova: CapacitorCordova:
:path: "../../node_modules/@capacitor/ios" :path: "../../node_modules/@capacitor/ios"
CapacitorFilesystem: CapacitorFilesystem:
@@ -121,6 +136,7 @@ SPEC CHECKSUMS:
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79 CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
@@ -138,7 +154,9 @@ SPEC CHECKSUMS:
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79 MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3 PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

3581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.4.6", "version": "0.4.8",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@@ -46,6 +46,7 @@
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal" "electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
}, },
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "6.0.2",
"@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor-mlkit/barcode-scanning": "^6.0.0",
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
@@ -166,11 +167,11 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "~5.2.2", "typescript": "~5.2.2",
"vite": "^5.2.0", "vite": "^5.2.0",
"vite-plugin-pwa": "^0.19.8" "vite-plugin-pwa": "^1.0.0"
}, },
"main": "./dist-electron/main.js", "main": "./dist-electron/main.js",
"build": { "build": {
"appId": "app.timesafari", "appId": "app.timesafari.app",
"productName": "TimeSafari", "productName": "TimeSafari",
"directories": { "directories": {
"output": "dist-electron-packages" "output": "dist-electron-packages"
@@ -181,7 +182,7 @@
], ],
"extraResources": [ "extraResources": [
{ {
"from": "dist", "from": "dist-electron/www",
"to": "www" "to": "www"
} }
], ],

View File

@@ -2,5 +2,6 @@ dependencies:
- gradle - gradle
- java - java
- pod - pod
- rubygems.org
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing). # other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).

View File

@@ -1,5 +1,6 @@
eth_keys eth_keys
pywebview pywebview
pyinstaller>=6.12.0 pyinstaller>=6.12.0
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
# For development # For development
watchdog>=3.0.0 # For file watching support watchdog>=3.0.0 # For file watching support

View File

@@ -1,10 +1,9 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
console.log('Starting electron build process...'); console.log('Starting electron build process...');
// Copy web files // Define paths
const webDistPath = path.join(__dirname, '..', 'dist');
const electronDistPath = path.join(__dirname, '..', 'dist-electron'); const electronDistPath = path.join(__dirname, '..', 'dist-electron');
const wwwPath = path.join(electronDistPath, 'www'); const wwwPath = path.join(electronDistPath, 'www');
@@ -13,231 +12,154 @@ if (!fs.existsSync(wwwPath)) {
fs.mkdirSync(wwwPath, { recursive: true }); fs.mkdirSync(wwwPath, { recursive: true });
} }
// Copy web files to www directory // Create a platform-specific index.html for Electron
fs.cpSync(webDistPath, wwwPath, { recursive: true }); const initialIndexContent = `<!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,viewport-fit=cover">
<link rel="icon" href="/favicon.ico">
<title>TimeSafari</title>
</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>
<script type="module">
// Force electron platform
window.process = { env: { VITE_PLATFORM: 'electron' } };
import('./src/main.electron.ts');
</script>
</body>
</html>`;
// Fix asset paths in index.html // Write the Electron-specific index.html
fs.writeFileSync(path.join(wwwPath, 'index.html'), initialIndexContent);
// Copy only necessary assets from web build
const webDistPath = path.join(__dirname, '..', 'dist');
if (fs.existsSync(webDistPath)) {
// Copy assets directory
const assetsSrc = path.join(webDistPath, 'assets');
const assetsDest = path.join(wwwPath, 'assets');
if (fs.existsSync(assetsSrc)) {
fs.cpSync(assetsSrc, assetsDest, { recursive: true });
}
// Copy favicon
const faviconSrc = path.join(webDistPath, 'favicon.ico');
if (fs.existsSync(faviconSrc)) {
fs.copyFileSync(faviconSrc, path.join(wwwPath, 'favicon.ico'));
}
}
// Remove service worker files
const swFilesToRemove = [
'sw.js',
'sw.js.map',
'workbox-*.js',
'workbox-*.js.map',
'registerSW.js',
'manifest.webmanifest',
'**/workbox-*.js',
'**/workbox-*.js.map',
'**/sw.js',
'**/sw.js.map',
'**/registerSW.js',
'**/manifest.webmanifest'
];
console.log('Removing service worker files...');
swFilesToRemove.forEach(pattern => {
const files = fs.readdirSync(wwwPath).filter(file =>
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
);
files.forEach(file => {
const filePath = path.join(wwwPath, file);
console.log(`Removing ${filePath}`);
try {
fs.unlinkSync(filePath);
} catch (err) {
console.warn(`Could not remove ${filePath}:`, err.message);
}
});
});
// Also check and remove from assets directory
const assetsPath = path.join(wwwPath, 'assets');
if (fs.existsSync(assetsPath)) {
swFilesToRemove.forEach(pattern => {
const files = fs.readdirSync(assetsPath).filter(file =>
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
);
files.forEach(file => {
const filePath = path.join(assetsPath, file);
console.log(`Removing ${filePath}`);
try {
fs.unlinkSync(filePath);
} catch (err) {
console.warn(`Could not remove ${filePath}:`, err.message);
}
});
});
}
// Modify index.html to remove service worker registration
const indexPath = path.join(wwwPath, 'index.html'); const indexPath = path.join(wwwPath, 'index.html');
let indexContent = fs.readFileSync(indexPath, 'utf8'); if (fs.existsSync(indexPath)) {
console.log('Modifying index.html to remove service worker registration...');
let indexContent = fs.readFileSync(indexPath, 'utf8');
// Remove service worker registration script
indexContent = indexContent
.replace(/<script[^>]*id="vite-plugin-pwa:register-sw"[^>]*><\/script>/g, '')
.replace(/<script[^>]*registerServiceWorker[^>]*><\/script>/g, '')
.replace(/<link[^>]*rel="manifest"[^>]*>/g, '')
.replace(/<link[^>]*rel="serviceworker"[^>]*>/g, '')
.replace(/navigator\.serviceWorker\.register\([^)]*\)/g, '')
.replace(/if\s*\(\s*['"]serviceWorker['"]\s*in\s*navigator\s*\)\s*{[^}]*}/g, '');
fs.writeFileSync(indexPath, indexContent);
console.log('Successfully modified index.html');
}
// Fix asset paths // Fix asset paths
indexContent = indexContent console.log('Fixing asset paths in index.html...');
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
modifiedIndexContent = modifiedIndexContent
.replace(/\/assets\//g, './assets/') .replace(/\/assets\//g, './assets/')
.replace(/href="\//g, 'href="./') .replace(/href="\//g, 'href="./')
.replace(/src="\//g, 'src="./'); .replace(/src="\//g, 'src="./');
fs.writeFileSync(indexPath, indexContent); fs.writeFileSync(indexPath, modifiedIndexContent);
// Verify no service worker references remain
const finalContent = fs.readFileSync(indexPath, 'utf8');
if (finalContent.includes('serviceWorker') || finalContent.includes('workbox')) {
console.warn('Warning: Service worker references may still exist in index.html');
}
// Check for remaining /assets/ paths // Check for remaining /assets/ paths
console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/')); console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
console.log('Sample of fixed content:', indexContent.substring(0, 500)); console.log('Sample of fixed content:', finalContent.substring(0, 500));
console.log('Copied and fixed web files in:', wwwPath); console.log('Copied and fixed web files in:', wwwPath);
// Copy main process files // Copy main process files
console.log('Copying main process files...'); console.log('Copying main process files...');
// Create the main process file with inlined logger // Copy the main process file instead of creating a template
const mainContent = `const { app, BrowserWindow } = require("electron"); const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
const path = require("path"); const mainDestPath = path.join(electronDistPath, 'main.js');
const fs = require("fs");
// Inline logger implementation if (fs.existsSync(mainSrcPath)) {
const logger = { fs.copyFileSync(mainSrcPath, mainDestPath);
log: (...args) => console.log(...args), console.log('Copied main process file successfully');
error: (...args) => console.error(...args), } else {
info: (...args) => console.info(...args), console.error('Main process file not found at:', mainSrcPath);
warn: (...args) => console.warn(...args), process.exit(1);
debug: (...args) => console.debug(...args),
};
// Check if running in dev mode
const isDev = process.argv.includes("--inspect");
function createWindow() {
// Add before createWindow function
const preloadPath = path.join(__dirname, "preload.js");
logger.log("Checking preload path:", preloadPath);
logger.log("Preload exists:", fs.existsSync(preloadPath));
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
allowRunningInsecureContent: false,
preload: path.join(__dirname, "preload.js"),
},
});
// Always open DevTools for now
mainWindow.webContents.openDevTools();
// Intercept requests to fix asset paths
mainWindow.webContents.session.webRequest.onBeforeRequest(
{
urls: [
"file://*/*/assets/*",
"file://*/assets/*",
"file:///assets/*", // Catch absolute paths
"<all_urls>", // Catch all URLs as a fallback
],
},
(details, callback) => {
let url = details.url;
// Handle paths that don't start with file://
if (!url.startsWith("file://") && url.includes("/assets/")) {
url = \`file://\${path.join(__dirname, "www", url)}\`;
}
// Handle absolute paths starting with /assets/
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
const baseDir = url.includes("dist-electron")
? url.substring(
0,
url.indexOf("/dist-electron") + "/dist-electron".length,
)
: \`file://\${__dirname}\`;
const assetPath = url.split("/assets/")[1];
const newUrl = \`\${baseDir}/www/assets/\${assetPath}\`;
callback({ redirectURL: newUrl });
return;
}
callback({}); // No redirect for other URLs
},
);
if (isDev) {
// Debug info
logger.log("Debug Info:");
logger.log("Running in dev mode:", isDev);
logger.log("App is packaged:", app.isPackaged);
logger.log("Process resource path:", process.resourcesPath);
logger.log("App path:", app.getAppPath());
logger.log("__dirname:", __dirname);
logger.log("process.cwd():", process.cwd());
}
const indexPath = path.join(__dirname, "www", "index.html");
if (isDev) {
logger.log("Loading index from:", indexPath);
logger.log("www path:", path.join(__dirname, "www"));
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
}
if (!fs.existsSync(indexPath)) {
logger.error(\`Index file not found at: \${indexPath}\`);
throw new Error("Index file not found");
}
// Add CSP headers to allow API connections, Google Fonts, and zxing-wasm
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [
"default-src 'self';" +
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app https://*.jsdelivr.net;" +
"img-src 'self' data: https: blob:;" +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
"font-src 'self' data: https://fonts.gstatic.com;" +
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
"worker-src 'self' blob:;",
],
},
});
},
);
// Load the index.html
mainWindow
.loadFile(indexPath)
.then(() => {
logger.log("Successfully loaded index.html");
if (isDev) {
mainWindow.webContents.openDevTools();
logger.log("DevTools opened - running in dev mode");
}
})
.catch((err) => {
logger.error("Failed to load index.html:", err);
logger.error("Attempted path:", indexPath);
});
// Listen for console messages from the renderer
mainWindow.webContents.on("console-message", (_event, _level, message) => {
logger.log("Renderer Console:", message);
});
// Add right after creating the BrowserWindow
mainWindow.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription) => {
logger.error("Page failed to load:", errorCode, errorDescription);
},
);
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
logger.error("Preload script error:", preloadPath, error);
});
mainWindow.webContents.on(
"console-message",
(_event, _level, message, line, sourceId) => {
logger.log("Renderer Console:", line, sourceId, message);
},
);
// Enable remote debugging when in dev mode
if (isDev) {
mainWindow.webContents.openDevTools();
}
} }
// Handle app ready console.log('Electron build process completed successfully');
app.whenReady().then(createWindow);
// Handle all windows closed
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Handle any errors
process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:", error);
});
`;
// Write the main process file
const mainDest = path.join(electronDistPath, 'main.js');
fs.writeFileSync(mainDest, mainContent);
// Copy preload script if it exists
const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
const preloadDest = path.join(electronDistPath, 'preload.js');
if (fs.existsSync(preloadSrc)) {
console.log(`Copying ${preloadSrc} to ${preloadDest}`);
fs.copyFileSync(preloadSrc, preloadDest);
}
// Verify build structure
console.log('\nVerifying build structure:');
console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
console.log('Build completed successfully!');

View File

@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
*/ */
function checkCommand(command, errorMessage) { function checkCommand(command, errorMessage) {
try { try {
execSync(command + ' --version', { stdio: 'ignore' }); execSync(command, { stdio: 'ignore' });
return true; return true;
} catch (e) { } catch (e) {
console.error(`${errorMessage}`); console.error(`${errorMessage}`);
@@ -164,10 +164,10 @@ function main() {
// Check required command line tools // Check required command line tools
// These are essential for building and testing the application // These are essential for building and testing the application
success &= checkCommand('node', 'Node.js is required'); success &= checkCommand('node --version', 'Node.js is required');
success &= checkCommand('npm', 'npm is required'); success &= checkCommand('npm --version', 'npm is required');
success &= checkCommand('gradle', 'Gradle is required for Android builds'); success &= checkCommand('gradle --version', 'Gradle is required for Android builds');
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds'); success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds');
// Check platform-specific development environments // Check platform-specific development environments
success &= checkAndroidSetup(); success &= checkAndroidSetup();

View File

@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
try { try {
// Stop the app before executing the deep link // Stop the app before executing the deep link
execSync('adb shell am force-stop app.timesafari'); execSync('adb shell am force-stop app.timesafari.app');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`); execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);

View File

@@ -4,7 +4,7 @@
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind --> <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert"> <NotificationGroup group="alert">
<div <div
class="fixed top-[calc(env(safe-area-inset-top)+1rem)] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end" class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
> >
<Notification <Notification
v-slot="{ notifications, close }" v-slot="{ notifications, close }"
@@ -330,8 +330,11 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
import { NotificationIface } from "./constants/app"; import { NotificationIface, USE_DEXIE_DB } from "./constants/app";
import * as databaseUtil from "./db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "./db/index";
import { logConsoleAndDb } from "./db/databaseUtil";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";
interface Settings { interface Settings {
@@ -396,7 +399,11 @@ export default class App extends Vue {
try { try {
logger.log("Retrieving settings for the active account..."); logger.log("Retrieving settings for the active account...");
const settings: Settings = await retrieveSettingsForActiveAccount(); let settings: Settings =
await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
logger.log("Retrieved settings:", settings); logger.log("Retrieved settings:", settings);
const notifyingNewActivity = !!settings?.notifyingNewActivityTime; const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
@@ -541,13 +548,13 @@ export default class App extends Vue {
<style> <style>
#Content { #Content {
padding-left: 1.5rem; padding-left: max(1.5rem, env(safe-area-inset-left));
padding-right: 1.5rem; padding-right: max(1.5rem, env(safe-area-inset-right));
padding-top: calc(env(safe-area-inset-top) + 1.5rem); padding-top: max(1.5rem, env(safe-area-inset-top));
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem); padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
} }
#QuickNav ~ #Content { #QuickNav ~ #Content {
padding-bottom: calc(env(safe-area-inset-bottom) + 6rem); padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
} }
</style> </style>

View File

@@ -14,22 +14,34 @@
class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2" class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div v-if="record.issuerDid"> <router-link
v-if="record.issuerDid && !isHiddenDid(record.issuerDid)"
:to="{
path: '/did/' + encodeURIComponent(record.issuerDid),
}"
title="More details about this person"
>
<EntityIcon <EntityIcon
:entity-id="record.issuerDid" :entity-id="record.issuerDid"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover" class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/> />
</div> </router-link>
<div v-else> <font-awesome
<font-awesome v-else-if="isHiddenDid(record.issuerDid)"
icon="person-circle-question" icon="eye-slash"
class="text-slate-300 text-[2rem]" class="text-slate-400 !size-[2rem] cursor-pointer"
/> @click="notifyHiddenPerson"
</div> />
<font-awesome
v-else
icon="person-circle-question"
class="text-slate-400 !size-[2rem] cursor-pointer"
@click="notifyUnknownPerson"
/>
<div> <div>
<h3 class="font-semibold"> <h3 v-if="record.issuer.known" class="font-semibold leading-tight">
{{ record.issuer.known ? record.issuer.displayName : "" }} {{ record.issuer.displayName }}
</h3> </h3>
<p class="ms-auto text-xs text-slate-500 italic"> <p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }} {{ friendlyDate }}
@@ -37,7 +49,11 @@
</div> </div>
</div> </div>
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)"> <a
class="cursor-pointer"
data-testid="circle-info-link"
@click="$emit('loadClaim', record.jwtId)"
>
<font-awesome icon="circle-info" class="fa-fw text-slate-500" /> <font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a> </a>
</div> </div>
@@ -46,7 +62,7 @@
<!-- Record Image --> <!-- Record Image -->
<div <div
v-if="record.image" v-if="record.image"
class="bg-cover mb-6 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4" class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`" :style="`background-image: url(${record.image});`"
> >
<a <a
@@ -62,29 +78,59 @@
</a> </a>
</div> </div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
<div <div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5" class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
> >
<!-- Source --> <!-- Source -->
<div <div
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3" class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
> >
<div class="relative w-fit mx-auto"> <div class="relative w-fit mx-auto">
<div> <div>
<!-- Project Icon --> <!-- Project Icon -->
<div v-if="record.providerPlanName"> <div v-if="record.providerPlanName">
<ProjectIcon <router-link
:entity-id="record.providerPlanName" :to="{
:icon-size="48" path:
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full" '/project/' +
/> encodeURIComponent(record.providerPlanHandleId || ''),
}"
title="View project details"
>
<ProjectIcon
:entity-id="record.providerPlanHandleId || ''"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</router-link>
</div> </div>
<!-- Identicon for DIDs --> <!-- Identicon for DIDs -->
<div v-else-if="record.agentDid"> <div v-else-if="record.agentDid">
<EntityIcon <router-link
:entity-id="record.agentDid" v-if="!isHiddenDid(record.agentDid)"
:profile-image-url="record.issuer.profileImageUrl" :to="{
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]" path: '/did/' + encodeURIComponent(record.agentDid),
}"
title="More details about this person"
>
<EntityIcon
:entity-id="record.agentDid"
:profile-image-url="record.issuer.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</router-link>
<font-awesome
v-else
icon="eye-slash"
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
@click="notifyHiddenPerson"
/> />
</div> </div>
<!-- Unknown Person --> <!-- Unknown Person -->
@@ -92,6 +138,7 @@
<font-awesome <font-awesome
icon="person-circle-question" icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]" class="text-slate-300 text-[3rem] sm:text-[4rem]"
@click="notifyUnknownPerson"
/> />
</div> </div>
</div> </div>
@@ -110,9 +157,11 @@
<!-- Arrow --> <!-- Arrow -->
<div <div
class="absolute inset-x-[8rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2" class="absolute inset-x-[7rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
> >
<div class="text-sm text-center leading-none font-semibold pe-[15px]"> <div
class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4"
>
{{ fetchAmount }} {{ fetchAmount }}
</div> </div>
@@ -129,24 +178,47 @@
<!-- Destination --> <!-- Destination -->
<div <div
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3" class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
> >
<div class="relative w-fit mx-auto"> <div class="relative w-fit mx-auto">
<div> <div>
<!-- Project Icon --> <!-- Project Icon -->
<div v-if="record.recipientProjectName"> <div v-if="record.recipientProjectName">
<ProjectIcon <router-link
:entity-id="record.recipientProjectName" :to="{
:icon-size="48" path:
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full" '/project/' +
/> encodeURIComponent(record.fulfillsPlanHandleId || ''),
}"
title="View project details"
>
<ProjectIcon
:entity-id="record.fulfillsPlanHandleId || ''"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</router-link>
</div> </div>
<!-- Identicon for DIDs --> <!-- Identicon for DIDs -->
<div v-else-if="record.recipientDid"> <div v-else-if="record.recipientDid">
<EntityIcon <router-link
:entity-id="record.recipientDid" v-if="!isHiddenDid(record.recipientDid)"
:profile-image-url="record.receiver.profileImageUrl" :to="{
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]" path: '/did/' + encodeURIComponent(record.recipientDid),
}"
title="More details about this person"
>
<EntityIcon
:entity-id="record.recipientDid"
:profile-image-url="record.receiver.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</router-link>
<font-awesome
v-else
icon="eye-slash"
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
@click="notifyHiddenPerson"
/> />
</div> </div>
<!-- Unknown Person --> <!-- Unknown Person -->
@@ -154,6 +226,7 @@
<font-awesome <font-awesome
icon="person-circle-question" icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]" class="text-slate-300 text-[3rem] sm:text-[4rem]"
@click="notifyUnknownPerson"
/> />
</div> </div>
</div> </div>
@@ -170,13 +243,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
</div> </div>
</li> </li>
</template> </template>
@@ -186,8 +252,9 @@ import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types"; import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util"; import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid } from "../libs/endorserServer"; import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { NotificationIface } from "../constants/app";
@Component({ @Component({
components: { components: {
@@ -202,6 +269,33 @@ export default class ActivityListItem extends Vue {
@Prop() activeDid!: string; @Prop() activeDid!: string;
@Prop() confirmerIdList?: string[]; @Prop() confirmerIdList?: string[];
isHiddenDid = isHiddenDid;
$notify!: (notification: NotificationIface, timeout?: number) => void;
notifyHiddenPerson() {
this.$notify(
{
group: "alert",
type: "warning",
title: "Person Outside Your Network",
text: "This person is not visible to you.",
},
3000,
);
}
notifyUnknownPerson() {
this.$notify(
{
group: "alert",
type: "warning",
title: "Unidentified Person",
text: "Nobody specific was recognized.",
},
3000,
);
}
@Emit() @Emit()
cacheImage(image: string) { cacheImage(image: string) {
return image; return image;
@@ -222,7 +316,7 @@ export default class ActivityListItem extends Vue {
const claim = const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim; (this.record.fullClaim as unknown).claim || this.record.fullClaim;
return `${claim.description}`; return `${claim?.description || ""}`;
} }
private displayAmount(code: string, amt: number) { private displayAmount(code: string, amt: number) {

View File

@@ -24,9 +24,7 @@ backup and database export, with platform-specific download instructions. * *
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="exportDatabase()" @click="exportDatabase()"
> >
Download Settings & Contacts Download Contacts
<br />
(excluding Identifier Data)
</button> </button>
<a <a
ref="downloadLink" ref="downloadLink"
@@ -62,14 +60,18 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator"; import { Component, Prop, Vue } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { import {
PlatformService, PlatformService,
PlatformCapabilities, PlatformCapabilities,
} from "../services/PlatformService"; } from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
/** /**
* @vue-component * @vue-component
@@ -131,21 +133,25 @@ export default class DataExportSection extends Vue {
*/ */
public async exportDatabase() { public async exportDatabase() {
try { try {
const blob = await db.export({ let allContacts: Contact[] = [];
prettyJson: true, const platformService = PlatformServiceFactory.getInstance();
transform: (table, value, key) => { const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (table === "contacts") { if (result) {
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them. allContacts = databaseUtil.mapQueryResultToValues(
Object.keys(value).forEach((prop) => { result,
if (value[prop] === undefined) { ) as unknown as Contact[];
delete value[prop]; }
} // if (USE_DEXIE_DB) {
}); // await db.open();
} // allContacts = await db.contacts.toArray();
return { value, key }; // }
},
}); // Convert contacts to export format
const fileName = `${db.name}-backup.json`; const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
if (this.platformCapabilities.hasFileDownload) { if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link // Web platform: Use download link
@@ -157,8 +163,9 @@ export default class DataExportSection extends Vue {
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) { } else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory // Native platform: Write to app directory
const content = await blob.text(); await this.platformService.writeAndShareFile(fileName, jsonStr);
await this.platformService.writeAndShareFile(fileName, content); } else {
throw new Error("This platform does not support file downloads.");
} }
this.$notify( this.$notify(
@@ -167,10 +174,10 @@ export default class DataExportSection extends Vue {
type: "success", type: "success",
title: "Export Successful", title: "Export Successful",
text: this.platformCapabilities.hasFileDownload text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format." ? "See your downloads directory for the backup."
: "You should have been prompted to save your backup file.", : "The backup file has been saved.",
}, },
-1, 3000,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Export Error:", error);

View File

@@ -99,8 +99,12 @@ import {
LTileLayer, LTileLayer,
} from "@vue-leaflet/vue-leaflet"; } from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { USE_DEXIE_DB } from "@/constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({ @Component({
components: { components: {
@@ -122,7 +126,10 @@ export default class FeedFilters extends Vue {
async open(onCloseIfChanged: () => void) { async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged; this.onCloseIfChanged = onCloseIfChanged;
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.hasVisibleDid = !!settings.filterFeedByVisible; this.hasVisibleDid = !!settings.filterFeedByVisible;
this.isNearby = !!settings.filterFeedByNearby; this.isNearby = !!settings.filterFeedByNearby;
if (settings.searchBoxes && settings.searchBoxes.length > 0) { if (settings.searchBoxes && settings.searchBoxes.length > 0) {
@@ -144,9 +151,17 @@ export default class FeedFilters extends Vue {
async toggleNearby() { async toggleNearby() {
this.settingChanged = true; this.settingChanged = true;
this.isNearby = !this.isNearby; this.isNearby = !this.isNearby;
await db.settings.update(MASTER_SETTINGS_KEY, { const platformService = PlatformServiceFactory.getInstance();
filterFeedByNearby: this.isNearby, await platformService.dbExec(
}); `UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
[this.isNearby, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
}
} }
async clearAll() { async clearAll() {
@@ -154,10 +169,18 @@ export default class FeedFilters extends Vue {
this.settingChanged = true; this.settingChanged = true;
} }
await db.settings.update(MASTER_SETTINGS_KEY, { const platformService = PlatformServiceFactory.getInstance();
filterFeedByNearby: false, await platformService.dbExec(
filterFeedByVisible: false, `UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
}); [false, false, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
}
this.hasVisibleDid = false; this.hasVisibleDid = false;
this.isNearby = false; this.isNearby = false;
@@ -168,10 +191,18 @@ export default class FeedFilters extends Vue {
this.settingChanged = true; this.settingChanged = true;
} }
await db.settings.update(MASTER_SETTINGS_KEY, { const platformService = PlatformServiceFactory.getInstance();
filterFeedByNearby: true, await platformService.dbExec(
filterFeedByVisible: true, `UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
}); [true, true, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
}
this.hasVisibleDid = true; this.hasVisibleDid = true;
this.isNearby = true; this.isNearby = true;

View File

@@ -89,7 +89,7 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
@@ -98,8 +98,10 @@ import {
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
@@ -144,11 +146,23 @@ export default class GiftedDialog extends Vue {
this.offerId = offerId || ""; this.offerId = offerId || "";
try { try {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.allContacts = await db.contacts.toArray(); const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
this.allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
@@ -306,10 +320,7 @@ export default class GiftedDialog extends Vue {
this.fromProjectId, this.fromProjectId,
); );
if ( if (!result.success) {
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result); const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result); logger.error("Error with give creation result:", result);
this.$notify( this.$notify(
@@ -356,15 +367,6 @@ export default class GiftedDialog extends Vue {
// Helper functions for readability // Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/** /**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message * @returns best guess at an error message

View File

@@ -74,10 +74,12 @@
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { AppString, NotificationIface } from "../constants/app"; import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index"; import { db } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { GiverReceiverInputInfo } from "../libs/util"; import { GiverReceiverInputInfo } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component @Component
export default class GivenPrompts extends Vue { export default class GivenPrompts extends Vue {
@@ -127,8 +129,16 @@ export default class GivenPrompts extends Vue {
this.visible = true; this.visible = true;
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo; this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
await db.open(); const platformService = PlatformServiceFactory.getInstance();
this.numContacts = await db.contacts.count(); const result = await platformService.dbQuery(
"SELECT COUNT(*) FROM contacts",
);
if (result) {
this.numContacts = result.values[0][0] as number;
}
if (USE_DEXIE_DB) {
this.numContacts = await db.contacts.count();
}
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
} }
@@ -229,10 +239,22 @@ export default class GivenPrompts extends Vue {
this.nextIdeaPastContacts(); this.nextIdeaPastContacts();
} else { } else {
// get the contact at that offset // get the contact at that offset
await db.open(); const platformService = PlatformServiceFactory.getInstance();
this.currentContact = await db.contacts const result = await platformService.dbQuery(
.offset(someContactDbIndex) "SELECT * FROM contacts LIMIT 1 OFFSET ?",
.first(); [someContactDbIndex],
);
if (result) {
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
someContactDbIndex
] as unknown as Contact;
}
if (USE_DEXIE_DB) {
await db.open();
this.currentContact = await db.contacts
.offset(someContactDbIndex)
.first();
}
this.shownContactDbIndices[someContactDbIndex] = true; this.shownContactDbIndices[someContactDbIndex] = true;
} }
} }

View File

@@ -48,11 +48,7 @@
<span> <span>
{{ didInfo(visDid) }} {{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)"> <span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<a <a :href="`/did/${visDid}`" class="text-blue-500">
:href="`/did/${visDid}`"
target="_blank"
class="text-blue-500"
>
<font-awesome <font-awesome
icon="arrow-up-right-from-square" icon="arrow-up-right-from-square"
class="fa-fw" class="fa-fw"

View File

@@ -4,7 +4,9 @@
<div class="text-lg text-center font-bold relative"> <div class="text-lg text-center font-bold relative">
<h1 id="ViewHeading" class="text-center font-bold"> <h1 id="ViewHeading" class="text-center font-bold">
<span v-if="uploading">Uploading Image&hellip;</span> <span v-if="uploading">Uploading Image&hellip;</span>
<span v-else-if="blob">Crop Image</span> <span v-else-if="blob">{{
crop ? "Crop Image" : "Preview Image"
}}</span>
<span v-else-if="showCameraPreview">Upload Image</span> <span v-else-if="showCameraPreview">Upload Image</span>
<span v-else>Add Photo</span> <span v-else>Add Photo</span>
</h1> </h1>
@@ -119,12 +121,23 @@
playsinline playsinline
muted muted
></video> ></video>
<button <div
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none" class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4"
@click="capturePhoto"
> >
<font-awesome icon="camera" class="w-[1em]" /> <button
</button> class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
@click="capturePhoto"
>
<font-awesome icon="camera" class="w-[1em]" />
</button>
<button
v-if="platformCapabilities.isMobile"
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
@click="rotateCamera"
>
<font-awesome icon="rotate" class="w-[1em]" />
</button>
</div>
</div> </div>
</div> </div>
<div <div
@@ -229,12 +242,12 @@
<p class="mb-2"> <p class="mb-2">
Before you can upload a photo, a friend needs to register you. Before you can upload a photo, a friend needs to register you.
</p> </p>
<router-link <button
:to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="handleQRCodeClick"
> >
Share Your Info Share Your Info
</router-link> </button>
</div> </div>
</template> </template>
</div> </div>
@@ -247,11 +260,17 @@ import axios from "axios";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app"; import { Capacitor } from "@capacitor/core";
import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto"; import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import * as databaseUtil from "../db/databaseUtil";
const inputImageFileNameRef = ref<Blob>(); const inputImageFileNameRef = ref<Blob>();
@@ -262,6 +281,11 @@ const inputImageFileNameRef = ref<Blob>();
type: Boolean, type: Boolean,
default: true, default: true,
}, },
defaultCameraMode: {
type: String,
default: "environment",
validator: (value: string) => ["environment", "user"].includes(value),
},
}, },
}) })
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
@@ -303,6 +327,9 @@ export default class ImageMethodDialog extends Vue {
/** Camera stream reference */ /** Camera stream reference */
private cameraStream: MediaStream | null = null; private cameraStream: MediaStream | null = null;
/** Current camera facing mode */
private currentFacingMode: "environment" | "user" = "environment";
private platformService = PlatformServiceFactory.getInstance(); private platformService = PlatformServiceFactory.getInstance();
URL = window.URL || window.webkitURL; URL = window.URL || window.webkitURL;
@@ -334,7 +361,10 @@ export default class ImageMethodDialog extends Vue {
*/ */
async mounted() { async mounted() {
try { try {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error retrieving settings from database:", error); logger.error("Error retrieving settings from database:", error);
@@ -361,15 +391,16 @@ export default class ImageMethodDialog extends Vue {
} }
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) { open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
logger.debug("ImageMethodDialog.open called");
this.claimType = claimType; this.claimType = claimType;
this.crop = !!crop; this.crop = !!crop;
this.imageCallback = setImageFn; this.imageCallback = setImageFn;
this.visible = true; this.visible = true;
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
// Start camera preview immediately if not on mobile // Start camera preview immediately
if (!this.platformCapabilities.isNativeApp) { logger.debug("Starting camera preview from open()");
this.startCameraPreview(); this.startCameraPreview();
}
} }
async uploadImageFile(event: Event) { async uploadImageFile(event: Event) {
@@ -438,46 +469,24 @@ export default class ImageMethodDialog extends Vue {
logger.debug("startCameraPreview called"); logger.debug("startCameraPreview called");
logger.debug("Current showCameraPreview state:", this.showCameraPreview); logger.debug("Current showCameraPreview state:", this.showCameraPreview);
logger.debug("Platform capabilities:", this.platformCapabilities); logger.debug("Platform capabilities:", this.platformCapabilities);
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
logger.debug(
"getUserMedia available:",
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
);
if (this.platformCapabilities.isNativeApp) {
logger.debug("Using platform service for mobile device");
this.cameraState = "initializing";
this.cameraStateMessage = "Using platform camera service...";
try {
const result = await this.platformService.takePicture();
this.blob = result.blob;
this.fileName = result.fileName;
this.cameraState = "ready";
this.cameraStateMessage = "Photo captured successfully";
} catch (error) {
logger.error("Error taking picture:", error);
this.cameraState = "error";
this.cameraStateMessage =
error instanceof Error ? error.message : "Failed to take picture";
this.error =
error instanceof Error ? error.message : "Failed to take picture";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to take picture. Please try again.",
},
5000,
);
}
return;
}
logger.debug("Starting camera preview for desktop browser");
try { try {
this.cameraState = "initializing"; this.cameraState = "initializing";
this.cameraStateMessage = "Requesting camera access..."; this.cameraStateMessage = "Requesting camera access...";
this.showCameraPreview = true; this.showCameraPreview = true;
await this.$nextTick(); await this.$nextTick();
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error("Camera API not available in this browser");
}
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" }, video: { facingMode: this.currentFacingMode },
}); });
logger.debug("Camera access granted"); logger.debug("Camera access granted");
this.cameraStream = stream; this.cameraStream = stream;
@@ -491,25 +500,36 @@ export default class ImageMethodDialog extends Vue {
videoElement.srcObject = stream; videoElement.srcObject = stream;
await new Promise((resolve) => { await new Promise((resolve) => {
videoElement.onloadedmetadata = () => { videoElement.onloadedmetadata = () => {
videoElement.play().then(() => { videoElement
resolve(true); .play()
}); .then(() => {
logger.debug("Video element started playing");
resolve(true);
})
.catch((error) => {
logger.error("Error playing video:", error);
throw error;
});
}; };
}); });
} else {
logger.error("Video element not found");
throw new Error("Video element not found");
} }
} catch (error) { } catch (error) {
logger.error("Error starting camera preview:", error); logger.error("Error starting camera preview:", error);
let errorMessage = let errorMessage =
error instanceof Error ? error.message : "Failed to access camera"; error instanceof Error ? error.message : "Failed to access camera";
if ( if (
error.name === "NotReadableError" || error instanceof Error &&
error.name === "TrackStartError" (error.name === "NotReadableError" || error.name === "TrackStartError")
) { ) {
errorMessage = errorMessage =
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again."; "Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
} else if ( } else if (
error.name === "NotAllowedError" || error instanceof Error &&
error.name === "PermissionDeniedError" (error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError")
) { ) {
errorMessage = errorMessage =
"Camera access was denied. Please allow camera access in your browser settings."; "Camera access was denied. Please allow camera access in your browser settings.";
@@ -517,6 +537,7 @@ export default class ImageMethodDialog extends Vue {
this.cameraState = "error"; this.cameraState = "error";
this.cameraStateMessage = errorMessage; this.cameraStateMessage = errorMessage;
this.error = errorMessage; this.error = errorMessage;
this.showCameraPreview = false;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -526,7 +547,6 @@ export default class ImageMethodDialog extends Vue {
}, },
5000, 5000,
); );
this.showCameraPreview = false;
} }
} }
@@ -578,6 +598,21 @@ export default class ImageMethodDialog extends Vue {
} }
} }
async rotateCamera() {
// Toggle between front and back cameras
this.currentFacingMode =
this.currentFacingMode === "environment" ? "user" : "environment";
// Stop current stream
if (this.cameraStream) {
this.cameraStream.getTracks().forEach((track) => track.stop());
this.cameraStream = null;
}
// Start new stream with updated facing mode
await this.startCameraPreview();
}
private createBlobURL(blob: Blob): string { private createBlobURL(blob: Blob): string {
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} }
@@ -612,6 +647,7 @@ export default class ImageMethodDialog extends Vue {
5000, 5000,
); );
this.uploading = false; this.uploading = false;
this.close();
return; return;
} }
formData.append("image", this.blob, this.fileName || "photo.jpg"); formData.append("image", this.blob, this.fileName || "photo.jpg");
@@ -666,6 +702,7 @@ export default class ImageMethodDialog extends Vue {
); );
this.uploading = false; this.uploading = false;
this.blob = undefined; this.blob = undefined;
this.close();
} }
} }
@@ -673,6 +710,14 @@ export default class ImageMethodDialog extends Vue {
toggleDiagnostics() { toggleDiagnostics() {
this.showDiagnostics = !this.showDiagnostics; this.showDiagnostics = !this.showDiagnostics;
} }
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
} }
</script> </script>

View File

@@ -172,8 +172,10 @@ import {
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { decryptMessage } from "../libs/crypto"; import { decryptMessage } from "../libs/crypto";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { NotificationIface } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Member { interface Member {
admitted: boolean; admitted: boolean;
@@ -209,7 +211,10 @@ export default class MembersList extends Vue {
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
async created() { async created() {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || ""; this.firstName = settings.firstName || "";
@@ -296,7 +301,7 @@ export default class MembersList extends Vue {
this.decryptedMembers.length === 0 || this.decryptedMembers.length === 0 ||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId this.decryptedMembers[0].member.memberId !== this.members[0].memberId
) { ) {
return "Your password is not the same as the organizer. Reload or have them check their password."; return "Your password is not the same as the organizer. Retry or have them check their password.";
} else { } else {
// the first (organizer) member was decrypted OK // the first (organizer) member was decrypted OK
return ""; return "";
@@ -337,7 +342,7 @@ export default class MembersList extends Vue {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Contact Exists", title: "Contact Exists",
text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.", text: "They are in your contacts. To remove them, use the contacts page.",
}, },
10000, 10000,
); );
@@ -347,7 +352,7 @@ export default class MembersList extends Vue {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Contact Available", title: "Contact Available",
text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.", text: "This is to add them to your contacts. To remove them later, use the contacts page.",
}, },
10000, 10000,
); );
@@ -355,7 +360,16 @@ export default class MembersList extends Vue {
} }
async loadContacts() { async loadContacts() {
this.contacts = await db.contacts.toArray(); const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery("SELECT * FROM contacts");
if (result) {
this.contacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
if (USE_DEXIE_DB) {
this.contacts = await db.contacts.toArray();
}
} }
getContactFor(did: string): Contact | undefined { getContactFor(did: string): Contact | undefined {
@@ -439,7 +453,14 @@ export default class MembersList extends Vue {
if (result.success) { if (result.success) {
decrMember.isRegistered = true; decrMember.isRegistered = true;
if (oldContact) { if (oldContact) {
await db.contacts.update(decrMember.did, { registered: true }); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET registered = ? WHERE did = ?",
[true, decrMember.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(decrMember.did, { registered: true });
}
oldContact.registered = true; oldContact.registered = true;
} }
this.$notify( this.$notify(
@@ -492,7 +513,14 @@ export default class MembersList extends Vue {
name: member.name, name: member.name,
}; };
await db.contacts.add(newContact); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"INSERT INTO contacts (did, name) VALUES (?, ?)",
[member.did, member.name],
);
if (USE_DEXIE_DB) {
await db.contacts.add(newContact);
}
this.contacts.push(newContact); this.contacts.push(newContact);
this.$notify( this.$notify(

View File

@@ -82,12 +82,13 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { import {
createAndSubmitOffer, createAndSubmitOffer,
serverMessageForUser, serverMessageForUser,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@@ -116,7 +117,10 @@ export default class OfferDialog extends Vue {
this.recipientDid = recipientDid; this.recipientDid = recipientDid;
this.recipientName = recipientName; this.recipientName = recipientName;
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
@@ -245,10 +249,7 @@ export default class OfferDialog extends Vue {
this.projectId, this.projectId,
); );
if ( if (!result.success) {
result.type === "error" ||
this.isOfferCreationError(result.response)
) {
const errorMessage = this.getOfferCreationErrorMessage(result); const errorMessage = this.getOfferCreationErrorMessage(result);
logger.error("Error with offer creation result:", result); logger.error("Error with offer creation result:", result);
this.$notify( this.$notify(
@@ -292,15 +293,6 @@ export default class OfferDialog extends Vue {
// Helper functions for readability // Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isOfferCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/** /**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message * @returns best guess at an error message

View File

@@ -5,7 +5,7 @@
<h1 class="text-xl font-bold text-center mb-4 relative"> <h1 class="text-xl font-bold text-center mb-4 relative">
Welcome to Time Safari Welcome to Time Safari
<br /> <br />
- Showcasing Gratitude & Magnifying Time - Showcase Impact & Magnify Time
<div <div
class="text-lg text-center leading-none absolute right-0 -top-1" class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)" @click="onClickClose(true)"
@@ -14,6 +14,9 @@
</div> </div>
</h1> </h1>
The feed underneath this pop-up shows the latest contributions, some from
people and some from projects.
<p v-if="isRegistered" class="mt-4"> <p v-if="isRegistered" class="mt-4">
You can now log things that you've seen: You can now log things that you've seen:
<span v-if="numContacts > 0"> <span v-if="numContacts > 0">
@@ -23,14 +26,10 @@
<span class="bg-green-600 text-white rounded-full"> <span class="bg-green-600 text-white rounded-full">
<font-awesome icon="plus" class="fa-fw" /> <font-awesome icon="plus" class="fa-fw" />
</span> </span>
button to express your appreciation for... whatever -- maybe thanks for button to express your appreciation for... whatever.
showing you all these fascinating stories of
<em>gratitude</em>.
</p> </p>
<p v-else class="mt-4"> <p class="mt-4">
The feed underneath this pop-up shows the latest gifts that others have Once someone registers you, you can log your appreciation, too.
recognized. Once someone registers you, you can log your appreciation,
too.
</p> </p>
<p class="mt-4"> <p class="mt-4">
@@ -201,13 +200,16 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { NotificationIface } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { import {
db, db,
retrieveSettingsForActiveAccount, retrieveSettingsForActiveAccount,
updateAccountSettings, updateAccountSettings,
} from "../db/index"; } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { OnboardPage } from "../libs/util"; import { OnboardPage } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
@Component({ @Component({
computed: { computed: {
@@ -222,7 +224,7 @@ export default class OnboardingDialog extends Vue {
$router!: Router; $router!: Router;
activeDid = ""; activeDid = "";
firstContactName = null; firstContactName = "";
givenName = ""; givenName = "";
isRegistered = false; isRegistered = false;
numContacts = 0; numContacts = 0;
@@ -231,29 +233,54 @@ export default class OnboardingDialog extends Vue {
async open(page: OnboardPage) { async open(page: OnboardPage) {
this.page = page; this.page = page;
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered; this.isRegistered = !!settings.isRegistered;
const contacts = await db.contacts.toArray(); const platformService = PlatformServiceFactory.getInstance();
this.numContacts = contacts.length; const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
if (this.numContacts > 0) { if (dbContacts) {
this.firstContactName = contacts[0].name; this.numContacts = dbContacts.values.length;
const firstContact = dbContacts.values[0];
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
firstContact,
]) as unknown as Contact;
this.firstContactName = fullContact.name || "";
}
if (USE_DEXIE_DB) {
const contacts = await db.contacts.toArray();
this.numContacts = contacts.length;
if (this.numContacts > 0) {
this.firstContactName = contacts[0].name || "";
}
} }
this.visible = true; this.visible = true;
if (this.page === OnboardPage.Create) { if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages // we'll assume that they've been through all the other pages
await updateAccountSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
} }
} }
async onClickClose(done?: boolean, goHome?: boolean) { async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false; this.visible = false;
if (done) { if (done) {
await updateAccountSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
if (goHome) { if (goHome) {
this.$router.push({ name: "home" }); this.$router.push({ name: "home" });
} }

View File

@@ -119,7 +119,12 @@ PhotoDialog.vue */
import axios from "axios"; import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app"; import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto"; import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@@ -173,9 +178,12 @@ export default class PhotoDialog extends Vue {
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
logger.log("PhotoDialog mounted"); // logger.log("PhotoDialog mounted");
try { try {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered; this.isRegistered = !!settings.isRegistered;
logger.log("isRegistered:", this.isRegistered); logger.log("isRegistered:", this.isRegistered);

View File

@@ -1,18 +1,14 @@
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<template> <template>
<a <a
v-if="linkToFull && imageUrl" v-if="linkToFullImage && imageUrl"
:href="imageUrl" :href="imageUrl"
target="_blank" target="_blank"
class="h-full w-full object-contain" class="h-full w-full object-contain"
> >
<div class="h-full w-full object-contain" v-html="generateIdenticon()" /> <div class="h-full w-full object-contain" v-html="generateIcon()" />
</a> </a>
<div <div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
v-else
class="h-full w-full object-contain"
v-html="generateIdenticon()"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { toSvg } from "jdenticon"; import { toSvg } from "jdenticon";
@@ -35,9 +31,9 @@ export default class ProjectIcon extends Vue {
@Prop entityId = ""; @Prop entityId = "";
@Prop iconSize = 0; @Prop iconSize = 0;
@Prop imageUrl = ""; @Prop imageUrl = "";
@Prop linkToFull = false; @Prop linkToFullImage = false;
generateIdenticon() { generateIcon() {
if (this.imageUrl) { if (this.imageUrl) {
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`; return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
} else { } else {

View File

@@ -102,7 +102,12 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app"; import {
DEFAULT_PUSH_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { import {
logConsoleAndDb, logConsoleAndDb,
retrieveSettingsForActiveAccount, retrieveSettingsForActiveAccount,
@@ -169,7 +174,10 @@ export default class PushNotificationPermission extends Vue {
this.isVisible = true; this.isVisible = true;
this.pushType = pushType; this.pushType = pushType;
try { try {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
let pushUrl = DEFAULT_PUSH_SERVER; let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) { if (settings?.webPushServer) {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="absolute right-5 top-[calc(env(safe-area-inset-top)+0.75rem)]"> <div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]">
<span class="align-center text-red-500 mr-2">{{ message }}</span> <span class="align-center text-red-500 mr-2">{{ message }}</span>
<span class="ml-2"> <span class="ml-2">
<router-link <router-link
@@ -15,7 +15,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator"; import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app"; import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
@Component @Component
@@ -28,7 +29,10 @@ export default class TopMessage extends Vue {
async mounted() { async mounted() {
try { try {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
if ( if (
settings.warnIfTestServer && settings.warnIfTestServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER

View File

@@ -37,9 +37,11 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component @Component
export default class UserNameDialog extends Vue { export default class UserNameDialog extends Vue {
@@ -61,15 +63,25 @@ export default class UserNameDialog extends Vue {
*/ */
async open(aCallback?: (name?: string) => void) { async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback; this.callback = aCallback || this.callback;
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.givenName = settings.firstName || ""; this.givenName = settings.firstName || "";
this.visible = true; this.visible = true;
} }
async onClickSaveChanges() { async onClickSaveChanges() {
await db.settings.update(MASTER_SETTINGS_KEY, { const platformService = PlatformServiceFactory.getInstance();
firstName: this.givenName, await platformService.dbExec(
}); "UPDATE settings SET firstName = ? WHERE id = ?",
[this.givenName, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.givenName,
});
}
this.visible = false; this.visible = false;
this.callback(this.givenName); this.callback(this.givenName);
} }

View File

@@ -3,6 +3,8 @@ import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils"; import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js"; import * as TWEEN from "@tweenjs/tween.js";
import { USE_DEXIE_DB } from "../../../../constants/app";
import * as databaseUtil from "../../../../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../../../../db"; import { retrieveSettingsForActiveAccount } from "../../../../db";
import { getHeaders } from "../../../../libs/endorserServer"; import { getHeaders } from "../../../../libs/endorserServer";
import { logger } from "../../../../utils/logger"; import { logger } from "../../../../utils/logger";
@@ -14,7 +16,10 @@ export async function loadLandmarks(vue, world, scene, loop) {
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS); vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
try { try {
const settings = await retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
const activeDid = settings.activeDid || ""; const activeDid = settings.activeDid || "";
const apiServer = settings.apiServer; const apiServer = settings.apiServer;
const headers = await getHeaders(activeDid); const headers = await getHeaders(activeDid);

View File

@@ -7,6 +7,7 @@ export enum AppString {
// This is used in titles and verbiage inside the app. // This is used in titles and verbiage inside the app.
// There is also an app name without spaces, for packaging in the package.json file used in the manifest. // There is also an app name without spaces, for packaging in the package.json file used in the manifest.
APP_NAME = "Time Safari", APP_NAME = "Time Safari",
APP_NAME_NO_SPACES = "TimeSafari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
@@ -32,24 +33,26 @@ export const APP_SERVER =
export const DEFAULT_ENDORSER_API_SERVER = export const DEFAULT_ENDORSER_API_SERVER =
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER || import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER; AppString.PROD_ENDORSER_API_SERVER;
export const DEFAULT_IMAGE_API_SERVER = export const DEFAULT_IMAGE_API_SERVER =
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER || import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER; AppString.PROD_IMAGE_API_SERVER;
export const DEFAULT_PARTNER_API_SERVER = export const DEFAULT_PARTNER_API_SERVER =
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER || import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
AppString.TEST_PARTNER_API_SERVER; AppString.PROD_PARTNER_API_SERVER;
export const DEFAULT_PUSH_SERVER = export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host; import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
export const IMAGE_TYPE_PROFILE = "profile"; export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED = export const PASSKEYS_ENABLED =
!!import.meta.env.VITE_PASSKEYS_ENABLED || false; !!import.meta.env.VITE_PASSKEYS_ENABLED || false;
export const USE_DEXIE_DB = false;
/** /**
* The possible values for "group" and "type" are in App.vue. * The possible values for "group" and "type" are in App.vue.
* Some of this comes from the notiwind package, some is custom. * Some of this comes from the notiwind package, some is custom.

View File

@@ -1,5 +1,31 @@
import migrationService from "../services/migrationService"; import migrationService from "../services/migrationService";
import type { QueryExecResult, SqlValue } from "../interfaces/database"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
// Generate a random secret for the secret table
// It's not really secure to maintain the secret next to the user's data.
// However, until we have better hooks into a real wallet or reliable secure
// storage, we'll do this for user convenience. As they sign more records
// and integrate with more people, they'll value it more and want to be more
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
// PWA so it's not in a browser... and then we hope to be integrated with a
// real wallet or something else more secure.
// One might ask: why encrypt at all? We figure a basic encryption is better
// than none. Plus, we expect to support their own password or keystore or
// external wallet as better signing options in the future, so it's gonna be
// important to have the structure where each account access might require
// user action.
// (Once upon a time we stored the secret in localStorage, but it frequently
// got erased, even though the IndexedDB still had the identity data. This
// ended up throwing lots of errors to the user... and they'd end up in a state
// where they couldn't take action because they couldn't unlock that identity.)
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const secretBase64 = arrayBufferToBase64(randomBytes);
// Each migration can include multiple SQL statements (with semicolons) // Each migration can include multiple SQL statements (with semicolons)
const MIGRATIONS = [ const MIGRATIONS = [
@@ -12,8 +38,8 @@ const MIGRATIONS = [
dateCreated TEXT NOT NULL, dateCreated TEXT NOT NULL,
derivationPath TEXT, derivationPath TEXT,
did TEXT NOT NULL, did TEXT NOT NULL,
identity TEXT, identityEncrBase64 TEXT, -- encrypted & base64-encoded
mnemonic TEXT, mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
passkeyCredIdHex TEXT, passkeyCredIdHex TEXT,
publicKeyHex TEXT NOT NULL publicKeyHex TEXT NOT NULL
); );
@@ -22,9 +48,11 @@ const MIGRATIONS = [
CREATE TABLE IF NOT EXISTS secret ( CREATE TABLE IF NOT EXISTS secret (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
secret TEXT NOT NULL secretBase64 TEXT NOT NULL
); );
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
accountDid TEXT, accountDid TEXT,
@@ -59,6 +87,8 @@ const MIGRATIONS = [
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid); CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
CREATE TABLE IF NOT EXISTS contacts ( CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL, did TEXT NOT NULL,
@@ -76,7 +106,7 @@ const MIGRATIONS = [
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs (
date TEXT PRIMARY KEY, date TEXT NOT NULL,
message TEXT NOT NULL message TEXT NOT NULL
); );
@@ -88,19 +118,21 @@ const MIGRATIONS = [
}, },
]; ];
export async function registerMigrations(): Promise<void> { /**
// Register all migrations * @param sqlExec - A function that executes a SQL statement and returns the result
for (const migration of MIGRATIONS) { * @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
await migrationService.registerMigration(migration); */
} export async function runMigrations<T>(
} sqlExec: (sql: string) => Promise<unknown>,
sqlQuery: (sql: string) => Promise<T>,
export async function runMigrations( extractMigrationNames: (result: T) => Set<string>,
sqlExec: (
sql: string,
params?: SqlValue[],
) => Promise<Array<QueryExecResult>>,
): Promise<void> { ): Promise<void> {
await registerMigrations(); for (const migration of MIGRATIONS) {
await migrationService.runMigrations(sqlExec); migrationService.registerMigration(migration);
}
await migrationService.runMigrations(
sqlExec,
sqlQuery,
extractMigrationNames,
);
} }

314
src/db/databaseUtil.ts Normal file
View File

@@ -0,0 +1,314 @@
/**
* This file is the SQL replacement of the index.ts file in the db directory.
* That file will eventually be deleted.
*/
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QueryExecResult } from "@/interfaces/database";
export async function updateDefaultSettings(
settingsChanges: Settings,
): Promise<boolean> {
delete settingsChanges.accountDid; // just in case
// ensure there is no "id" that would override the key
delete settingsChanges.id;
try {
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = generateUpdateStatement(
settingsChanges,
"settings",
"id = ?",
[MASTER_SETTINGS_KEY],
);
const result = await platformService.dbExec(sql, params);
return result.changes === 1;
} catch (error) {
logger.error("Error updating default settings:", error);
if (error instanceof Error) {
throw error; // Re-throw if it's already an Error with a message
} else {
throw new Error(
`Failed to update settings. We recommend you try again or restart the app.`,
);
}
}
}
export async function updateAccountSettings(
accountDid: string,
settingsChanges: Settings,
): Promise<boolean> {
settingsChanges.accountDid = accountDid;
delete settingsChanges.id; // key off account, not ID
const platform = PlatformServiceFactory.getInstance();
// First try to update existing record
const { sql: updateSql, params: updateParams } = generateUpdateStatement(
settingsChanges,
"settings",
"accountDid = ?",
[accountDid],
);
const updateResult = await platform.dbExec(updateSql, updateParams);
return updateResult.changes === 1;
}
const DEFAULT_SETTINGS: Settings = {
id: MASTER_SETTINGS_KEY,
activeDid: undefined,
apiServer: DEFAULT_ENDORSER_API_SERVER,
};
// retrieves default settings
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
const platform = PlatformServiceFactory.getInstance();
const sql = "SELECT * FROM settings WHERE id = ?";
const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
if (!result) {
return DEFAULT_SETTINGS;
} else {
const settings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
if (settings.searchBoxes) {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
}
return settings;
}
}
/**
* Retrieves settings for the active account, merging with default settings
*
* @returns Promise<Settings> Combined settings with account-specific overrides
* @throws Will log specific errors for debugging but returns default settings on failure
*/
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
try {
// Get default settings first
const defaultSettings = await retrieveSettingsForDefaultAccount();
// If no active DID, return defaults
if (!defaultSettings.activeDid) {
logConsoleAndDb(
"[databaseUtil] No active DID found, returning default settings",
);
return defaultSettings;
}
// Get account-specific settings
try {
const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[defaultSettings.activeDid],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
);
return defaultSettings;
}
// Map and filter settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
);
// Merge settings
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
// Handle searchBoxes parsing
if (settings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
}
return settings;
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
true,
);
// Return defaults on error
return defaultSettings;
}
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve default settings: ${error}`,
true,
);
// Return minimal default settings on complete failure
return {
id: MASTER_SETTINGS_KEY,
activeDid: undefined,
apiServer: DEFAULT_ENDORSER_API_SERVER,
};
}
}
let lastCleanupDate: string | null = null;
export let memoryLogs: string[] = [];
/**
* Logs a message to the database with proper handling of concurrent writes
* @param message - The message to log
* @author Matthew Raymer
*/
export async function logToDb(message: string): Promise<void> {
const platform = PlatformServiceFactory.getInstance();
const todayKey = new Date().toDateString();
const nowKey = new Date().toISOString();
try {
memoryLogs.push(`${new Date().toISOString()} ${message}`);
// Try to insert first, if it fails due to UNIQUE constraint, update instead
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
nowKey,
message,
]);
// Clean up old logs (keep only last 7 days) - do this less frequently
// Only clean up if the date is different from the last cleanup
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
const sevenDaysAgo = new Date(
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
);
memoryLogs = memoryLogs.filter(
(log) => log.split(" ")[0] > sevenDaysAgo.toDateString(),
);
await platform.dbExec("DELETE FROM logs WHERE date < ?", [
sevenDaysAgo.toDateString(),
]);
lastCleanupDate = todayKey;
}
} catch (error) {
// Log to console as fallback
// eslint-disable-next-line no-console
console.error(
"Error logging to database:",
error,
" ... for original message:",
message,
);
}
}
// similar method is in the sw_scripts/additional-scripts.js file
export async function logConsoleAndDb(
message: string,
isError = false,
): Promise<void> {
if (isError) {
logger.error(`${new Date().toISOString()} ${message}`);
} else {
logger.log(`${new Date().toISOString()} ${message}`);
}
await logToDb(message);
}
/**
* Generates an SQL INSERT statement and parameters from a model object.
* @param model The model object containing fields to update
* @param tableName The name of the table to update
* @returns Object containing the SQL statement and parameters array
*/
export function generateInsertStatement(
model: Record<string, unknown>,
tableName: string,
): { sql: string; params: unknown[] } {
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
const values = Object.values(model).filter((value) => value !== undefined);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
return {
sql: insertSql,
params: values,
};
}
/**
* Generates an SQL UPDATE statement and parameters from a model object.
* @param model The model object containing fields to update
* @param tableName The name of the table to update
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
* @param whereParams Parameters for the WHERE clause
* @returns Object containing the SQL statement and parameters array
*/
export function generateUpdateStatement(
model: Record<string, unknown>,
tableName: string,
whereClause: string,
whereParams: unknown[] = [],
): { sql: string; params: unknown[] } {
// Filter out undefined/null values and create SET clause
const setClauses: string[] = [];
const params: unknown[] = [];
Object.entries(model).forEach(([key, value]) => {
setClauses.push(`${key} = ?`);
params.push(value ?? null);
});
if (setClauses.length === 0) {
throw new Error("No valid fields to update");
}
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
return {
sql,
params: [...params, ...whereParams],
};
}
export function mapQueryResultToValues(
record: QueryExecResult | undefined,
): Array<Record<string, unknown>> {
if (!record) {
return [];
}
return mapColumnsToValues(record.columns, record.values) as Array<
Record<string, unknown>
>;
}
/**
* Maps an array of column names to an array of value arrays, creating objects where each column name
* is mapped to its corresponding value.
* @param columns Array of column names to use as object keys
* @param values Array of value arrays, where each inner array corresponds to one row of data
* @returns Array of objects where each object maps column names to their corresponding values
*/
export function mapColumnsToValues(
columns: string[],
values: unknown[][],
): Array<Record<string, unknown>> {
return values.map((row) => {
const obj: Record<string, unknown> = {};
columns.forEach((column, index) => {
obj[column] = row[index];
});
return obj;
});
}

View File

@@ -1,3 +1,9 @@
/**
* This is the original IndexedDB version of the database.
* It will eventually be replaced fully by the SQL version in databaseUtil.ts.
* Turn this on or off with the USE_DEXIE_DB constant in constants/app.ts.
*/
import BaseDexie, { Table } from "dexie"; import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon"; import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import * as R from "ramda"; import * as R from "ramda";
@@ -26,8 +32,8 @@ type NonsensitiveTables = {
}; };
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings // Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T; type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T; type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
BaseDexie & T; BaseDexie & T;

View File

@@ -45,6 +45,12 @@ export type Account = {
publicKeyHex: string; publicKeyHex: string;
}; };
// When finished with USE_DEXIE_DB, move these fields to Account and move identity and mnemonic here.
export type AccountEncrypted = Account & {
identityEncrBase64: string;
mnemonicEncrBase64: string;
};
/** /**
* Schema for the accounts table in the database. * Schema for the accounts table in the database.
* Fields starting with a $ character are encrypted. * Fields starting with a $ character are encrypted.

View File

@@ -25,6 +25,25 @@ function createWindow(): void {
logger.log("Checking preload path:", preloadPath); logger.log("Checking preload path:", preloadPath);
logger.log("Preload exists:", fs.existsSync(preloadPath)); logger.log("Preload exists:", fs.existsSync(preloadPath));
// Log environment and paths
logger.log("process.cwd():", process.cwd());
logger.log("__dirname:", __dirname);
logger.log("app.getAppPath():", app.getAppPath());
logger.log("app.isPackaged:", app.isPackaged);
// List files in __dirname and __dirname/www
try {
logger.log("Files in __dirname:", fs.readdirSync(__dirname));
const wwwDir = path.join(__dirname, "www");
if (fs.existsSync(wwwDir)) {
logger.log("Files in www:", fs.readdirSync(wwwDir));
} else {
logger.log("www directory does not exist in __dirname");
}
} catch (e) {
logger.error("Error reading directories:", e);
}
// Create the browser window. // Create the browser window.
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 1200, width: 1200,
@@ -88,7 +107,16 @@ function createWindow(): void {
logger.log("process.cwd():", process.cwd()); logger.log("process.cwd():", process.cwd());
} }
const indexPath = path.join(__dirname, "www", "index.html"); let indexPath = path.resolve(__dirname, "dist-electron", "www", "index.html");
if (!fs.existsSync(indexPath)) {
// Fallback for dev mode
indexPath = path.resolve(
process.cwd(),
"dist-electron",
"www",
"index.html",
);
}
if (isDev) { if (isDev) {
logger.log("Loading index from:", indexPath); logger.log("Loading index from:", indexPath);

View File

@@ -2,24 +2,33 @@ const { contextBridge, ipcRenderer } = require("electron");
const logger = { const logger = {
log: (message, ...args) => { log: (message, ...args) => {
// Always log in development, log with context in production
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log(message, ...args); console.log(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
} }
}, },
warn: (message, ...args) => { warn: (message, ...args) => {
if (process.env.NODE_ENV !== "production") { // Always log warnings
/* eslint-disable no-console */ /* eslint-disable no-console */
console.warn(message, ...args); console.warn(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
}
}, },
error: (message, ...args) => { error: (message, ...args) => {
// Always log errors
/* eslint-disable no-console */ /* eslint-disable no-console */
console.error(message, ...args); // Errors should always be logged console.error(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
}, },
info: (message, ...args) => {
// Always log info in development, log with context in production
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.info(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
}
},
}; };
// Use a more direct path resolution approach // Use a more direct path resolution approach
@@ -41,7 +50,10 @@ const getPath = (pathType) => {
} }
}; };
logger.log("Preload script starting..."); logger.info("Preload script starting...");
// Force electron platform in the renderer process
window.process = { env: { VITE_PLATFORM: "electron" } };
try { try {
contextBridge.exposeInMainWorld("electronAPI", { contextBridge.exposeInMainWorld("electronAPI", {
@@ -65,6 +77,7 @@ try {
env: { env: {
isElectron: true, isElectron: true,
isDev: process.env.NODE_ENV === "development", isDev: process.env.NODE_ENV === "development",
platform: "electron", // Explicitly set platform
}, },
// Path utilities // Path utilities
getBasePath: () => { getBasePath: () => {
@@ -72,7 +85,7 @@ try {
}, },
}); });
logger.log("Preload script completed successfully"); logger.info("Preload script completed successfully");
} catch (error) { } catch (error) {
logger.error("Error in preload script:", error); logger.error("Error in preload script:", error);
} }

59
src/interfaces/absurd-sql.d.ts vendored Normal file
View File

@@ -0,0 +1,59 @@
import type { QueryExecResult, SqlValue } from "./database";
declare module "@jlongster/sql.js" {
interface SQL {
Database: new (path: string, options?: { filename: boolean }) => AbsurdSqlDatabase;
FS: {
mkdir: (path: string) => void;
mount: (fs: any, options: any, path: string) => void;
open: (path: string, flags: string) => any;
close: (stream: any) => void;
};
register_for_idb: (fs: any) => void;
}
interface AbsurdSqlDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (
sql: string,
params?: unknown[],
) => Promise<{ changes: number; lastId?: number }>;
}
const initSqlJs: (options?: {
locateFile?: (file: string) => string;
}) => Promise<SQL>;
export default initSqlJs;
}
declare module "absurd-sql" {
import type { SQL } from "@jlongster/sql.js";
export class SQLiteFS {
constructor(fs: any, backend: any);
}
}
declare module "absurd-sql/dist/indexeddb-backend" {
export default class IndexedDBBackend {
constructor();
}
}
declare module "absurd-sql/dist/indexeddb-main-thread" {
export interface SQLiteOptions {
filename?: string;
autoLoad?: boolean;
debug?: boolean;
}
export interface SQLiteDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
close: () => Promise<void>;
}
export function initSqlJs(options?: any): Promise<any>;
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
}

View File

@@ -1,15 +1,24 @@
import { GenericVerifiableCredential } from "./common"; /**
* Types of Claims
*
* Note that these are for the claims that get signed.
* Records that are the latest edited entities are in the records.ts file.
*
*/
export interface AgreeVerifiableCredential { import { ClaimObject } from "./common";
"@context": string;
export interface AgreeActionClaim extends ClaimObject {
"@context": "https://schema.org";
"@type": string; "@type": string;
object: Record<string, unknown>; object: Record<string, unknown>;
} }
// Note that previous VCs may have additional fields. // Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4 // https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential extends GenericVerifiableCredential { export interface GiveActionClaim extends ClaimObject {
"@context"?: string; // context is optional because it might be embedded in another claim, eg. an AgreeAction
"@context"?: "https://schema.org";
"@type": "GiveAction"; "@type": "GiveAction";
agent?: { identifier: string }; agent?: { identifier: string };
description?: string; description?: string;
@@ -17,16 +26,25 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
identifier?: string; identifier?: string;
image?: string; image?: string;
object?: { amountOfThisGood: number; unitCode: string }; object?: { amountOfThisGood: number; unitCode: string };
provider?: GenericVerifiableCredential; provider?: ClaimObject;
recipient?: { identifier: string }; recipient?: { identifier: string };
} }
export interface JoinActionClaim extends ClaimObject {
agent?: { identifier: string };
event?: { organizer?: { name: string }; name?: string; startTime?: string };
}
// Note that previous VCs may have additional fields. // Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8 // https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential extends GenericVerifiableCredential { export interface OfferClaim extends ClaimObject {
"@context"?: string; "@context": "https://schema.org";
"@type": "Offer"; "@type": "Offer";
agent?: { identifier: string };
description?: string; description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string;
image?: string;
includesObject?: { amountOfThisGood: number; unitCode: string }; includesObject?: { amountOfThisGood: number; unitCode: string };
itemOffered?: { itemOffered?: {
description?: string; description?: string;
@@ -37,14 +55,18 @@ export interface OfferVerifiableCredential extends GenericVerifiableCredential {
name?: string; name?: string;
}; };
}; };
offeredBy?: { identifier: string }; offeredBy?: {
type?: "Person";
identifier: string;
};
provider?: ClaimObject;
recipient?: { identifier: string }; recipient?: { identifier: string };
validThrough?: string; validThrough?: string;
} }
// Note that previous VCs may have additional fields. // Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7 // https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential extends GenericVerifiableCredential { export interface PlanActionClaim extends ClaimObject {
"@context": "https://schema.org"; "@context": "https://schema.org";
"@type": "PlanAction"; "@type": "PlanAction";
name: string; name: string;
@@ -58,11 +80,18 @@ export interface PlanVerifiableCredential extends GenericVerifiableCredential {
} }
// AKA Registration & RegisterAction // AKA Registration & RegisterAction
export interface RegisterVerifiableCredential { export interface RegisterActionClaim extends ClaimObject {
"@context": string; "@context": "https://schema.org";
"@type": "RegisterAction"; "@type": "RegisterAction";
agent: { identifier: string }; agent: { identifier: string };
identifier?: string; identifier?: string;
object: string; object?: string;
participant?: { identifier: string }; participant?: { identifier: string };
} }
export interface TenureClaim extends ClaimObject {
"@context": "https://endorser.ch";
"@type": "Tenure";
party?: { identifier: string };
spatialUnit?: { geo?: { polygon?: string } };
}

View File

@@ -34,3 +34,77 @@ export interface ErrorResult extends ResultWithType {
type: "error"; type: "error";
error: InternalError; error: InternalError;
} }
export interface KeyMeta {
did: string;
publicKeyHex: string;
derivationPath?: string;
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
}
export interface KeyMetaMaybeWithPrivate extends KeyMeta {
mnemonic?: string; // 12 or 24 words encoding the seed
identity?: string; // Stringified IIdentifier object from Veramo
}
export interface KeyMetaWithPrivate extends KeyMeta {
mnemonic: string; // 12 or 24 words encoding the seed
identity: string; // Stringified IIdentifier object from Veramo
}
export interface QuantitativeValue extends GenericVerifiableCredential {
"@type": "QuantitativeValue";
"@context"?: string;
amountOfThisGood: number;
unitCode: string;
}
export interface AxiosErrorResponse {
message?: string;
response?: {
data?: {
error?: {
message?: string;
};
[key: string]: unknown;
};
status?: number;
config?: unknown;
};
config?: unknown;
[key: string]: unknown;
}
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
export interface CreateAndSubmitClaimResult {
success: boolean;
error?: string;
handleId?: string;
}
export interface Agent {
identifier?: string;
did?: string;
}
export interface ClaimObject {
"@type": string;
"@context"?: string;
[key: string]: unknown;
}
export interface VerifiableCredentialClaim {
"@context"?: string;
"@type": string;
type: string[];
credentialSubject: ClaimObject;
[key: string]: unknown;
}

View File

@@ -12,6 +12,4 @@ export interface DatabaseService {
sql: string, sql: string,
params?: unknown[], params?: unknown[],
): Promise<{ changes: number; lastId?: number }>; ): Promise<{ changes: number; lastId?: number }>;
getOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
getAll(sql: string, params?: unknown[]): Promise<unknown[][]>;
} }

View File

@@ -1,13 +1,17 @@
/**
* Interfaces for the give records with limited contact information, good to show on a feed.
**/
import { GiveSummaryRecord } from "./records"; import { GiveSummaryRecord } from "./records";
// Common interface for contact information // Common interface for views with summary contact information
export interface ContactInfo { export interface ContactInfo {
known: boolean; known: boolean;
displayName: string; displayName: string;
profileImageUrl?: string; profileImageUrl?: string;
} }
// Define the contact information fields // Define a subset of contact information fields
interface GiveContactInfo { interface GiveContactInfo {
giver: ContactInfo; giver: ContactInfo;
issuer: ContactInfo; issuer: ContactInfo;
@@ -17,5 +21,5 @@ interface GiveContactInfo {
image?: string; image?: string;
} }
// Combine GiveSummaryRecord with contact information using intersection type // Combine GiveSummaryRecord with contact information
export type GiveRecordWithContactInfo = GiveSummaryRecord & GiveContactInfo; export type GiveRecordWithContactInfo = GiveSummaryRecord & GiveContactInfo;

View File

@@ -1,7 +1,37 @@
export * from "./claims"; export type {
export * from "./claims-result"; // From common.ts
export * from "./common"; GenericCredWrapper,
GenericVerifiableCredential,
KeyMeta,
// Exclude types that are also exported from other files
// GiveVerifiableCredential,
// OfferVerifiableCredential,
// RegisterVerifiableCredential,
// PlanSummaryRecord,
// UserInfo,
} from "./common";
export type {
// From claims.ts
GiveActionClaim,
OfferClaim,
RegisterActionClaim,
} from "./claims";
export type {
// From claims-result.ts
CreateAndSubmitClaimResult,
} from "./claims-result";
export type {
// From records.ts
PlanSummaryRecord,
} from "./records";
export type {
// From user.ts
UserInfo,
} from "./user";
export * from "./limits"; export * from "./limits";
export * from "./records";
export * from "./user";
export * from "./deepLinks"; export * from "./deepLinks";

View File

@@ -1,14 +1,14 @@
import { GiveVerifiableCredential, OfferVerifiableCredential } from "./claims"; import { GiveActionClaim, OfferClaim } from "./claims";
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord { export interface GiveSummaryRecord {
[x: string]: PropertyKey | undefined | GiveVerifiableCredential; [x: string]: PropertyKey | undefined | GiveActionClaim;
type?: string; type?: string;
agentDid: string; agentDid: string;
amount: number; amount: number;
amountConfirmed: number; amountConfirmed: number;
description: string; description: string;
fullClaim: GiveVerifiableCredential; fullClaim: GiveActionClaim;
fulfillsHandleId: string; fulfillsHandleId: string;
fulfillsPlanHandleId?: string; fulfillsPlanHandleId?: string;
fulfillsType?: string; fulfillsType?: string;
@@ -26,7 +26,7 @@ export interface OfferSummaryRecord {
amount: number; amount: number;
amountGiven: number; amountGiven: number;
amountGivenConfirmed: number; amountGivenConfirmed: number;
fullClaim: OfferVerifiableCredential; fullClaim: OfferClaim;
fulfillsPlanHandleId: string; fulfillsPlanHandleId: string;
handleId: string; handleId: string;
issuerDid: string; issuerDid: string;

View File

@@ -32,7 +32,7 @@ export const newIdentifier = (
publicHex: string, publicHex: string,
privateHex: string, privateHex: string,
derivationPath: string, derivationPath: string,
): Omit<IIdentifier, keyof "provider"> => { ): IIdentifier => {
return { return {
did: DEFAULT_DID_PROVIDER_NAME + ":" + address, did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
keys: [ keys: [
@@ -159,7 +159,7 @@ export const nextDerivationPath = (origDerivPath: string) => {
}; };
// Base64 encoding/decoding utilities for browser // Base64 encoding/decoding utilities for browser
function base64ToArrayBuffer(base64: string): Uint8Array { export function base64ToArrayBuffer(base64: string): Uint8Array {
const binaryString = atob(base64); const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length); const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) { for (let i = 0; i < binaryString.length; i++) {
@@ -168,7 +168,7 @@ function base64ToArrayBuffer(base64: string): Uint8Array {
return bytes; return bytes;
} }
function arrayBufferToBase64(buffer: ArrayBuffer): string { export function arrayBufferToBase64(buffer: ArrayBuffer): string {
const binary = String.fromCharCode(...new Uint8Array(buffer)); const binary = String.fromCharCode(...new Uint8Array(buffer));
return btoa(binary); return btoa(binary);
} }
@@ -178,7 +178,7 @@ const IV_LENGTH = 12;
const KEY_LENGTH = 256; const KEY_LENGTH = 256;
const ITERATIONS = 100000; const ITERATIONS = 100000;
// Encryption helper function // Message encryption helper function, used for onboarding meeting messages
export async function encryptMessage(message: string, password: string) { export async function encryptMessage(message: string, password: string) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
@@ -226,7 +226,7 @@ export async function encryptMessage(message: string, password: string) {
return btoa(JSON.stringify(result)); return btoa(JSON.stringify(result));
} }
// Decryption helper function // Message decryption helper function, used for onboarding meeting messages
export async function decryptMessage(encryptedJson: string, password: string) { export async function decryptMessage(encryptedJson: string, password: string) {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson)); const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
@@ -273,7 +273,7 @@ export async function decryptMessage(encryptedJson: string, password: string) {
} }
// Test function to verify encryption/decryption // Test function to verify encryption/decryption
export async function testEncryptionDecryption() { export async function testMessageEncryptionDecryption() {
try { try {
const testMessage = "Hello, this is a test message! 🚀"; const testMessage = "Hello, this is a test message! 🚀";
const testPassword = "myTestPassword123"; const testPassword = "myTestPassword123";
@@ -299,9 +299,111 @@ export async function testEncryptionDecryption() {
logger.log("\nTesting with wrong password..."); logger.log("\nTesting with wrong password...");
try { try {
await decryptMessage(encrypted, "wrongPassword"); await decryptMessage(encrypted, "wrongPassword");
logger.log("Should not reach here"); logger.log("Incorrectly decrypted with wrong password ❌");
} catch (error) { } catch (error) {
logger.log("Correctly failed with wrong password ✅"); logger.log("Correctly failed to decrypt with wrong password ✅");
}
return success;
} catch (error) {
logger.error("Test failed with error:", error);
return false;
}
}
// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
export async function simpleEncrypt(
text: string,
secret: ArrayBuffer,
): Promise<ArrayBuffer> {
const iv = crypto.getRandomValues(new Uint8Array(16));
// Derive a 256-bit key from the secret using SHA-256
const keyData = await crypto.subtle.digest("SHA-256", secret);
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM" },
false,
["encrypt"],
);
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode(text),
);
// Combine IV and encrypted data
const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);
return result.buffer;
}
// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
export async function simpleDecrypt(
encryptedText: ArrayBuffer,
secret: ArrayBuffer,
): Promise<string> {
const data = new Uint8Array(encryptedText);
// Extract IV and encrypted data
const iv = data.slice(0, 16);
const encrypted = data.slice(16);
// Derive the same 256-bit key from the secret using SHA-256
const keyData = await crypto.subtle.digest("SHA-256", secret);
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM" },
false,
["decrypt"],
);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
encrypted,
);
return new TextDecoder().decode(decrypted);
}
// Test function for simple encryption/decryption
export async function testSimpleEncryptionDecryption() {
try {
const testMessage = "Hello, this is a test message! 🚀";
const testSecret = crypto.getRandomValues(new Uint8Array(32));
logger.log("Original message:", testMessage);
// Test encryption
logger.log("Encrypting...");
const encrypted = await simpleEncrypt(testMessage, testSecret);
const encryptedBase64 = arrayBufferToBase64(encrypted);
logger.log("Encrypted result:", encryptedBase64);
// Test decryption
logger.log("Decrypting...");
const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
logger.log("Decrypted result:", decrypted);
// Verify
const success = testMessage === decrypted;
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
logger.log("Messages match:", success);
// Test with wrong secret
logger.log("\nTesting with wrong secret...");
try {
await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
logger.log("Incorrectly decrypted with wrong secret ❌");
} catch (error) {
logger.log("Correctly failed to decrypt with wrong secret ✅");
} }
return success; return success;

Some files were not shown because too many files have changed in this diff Show More