Compare commits

..

13 Commits

Author SHA1 Message Date
Matthew Raymer
5ee4a7e411 refactor: centralize platform-specific behavior in platform services
- Add platform-specific capability methods to PlatformService interface:
  - getExportInstructions()
  - getExportSuccessMessage()
  - needsSecondaryDownloadLink()
  - needsDownloadCleanup()

- Update platform service implementations:
  - WebPlatformService: Implement web-specific export behavior
  - CapacitorPlatformService: Implement mobile-specific export behavior
  - ElectronPlatformService: Add placeholder for export functionality
  - PyWebViewPlatformService: Add placeholder for export functionality

- Refactor DataExportSection component:
  - Remove direct platform checks (isWeb, isCapacitor, etc.)
  - Use platform service capabilities for UI behavior
  - Improve error handling and logging
  - Add proper cleanup for web platform downloads

- Update PlatformServiceFactory:
  - Make getInstance() async to support dynamic imports
  - Improve platform service initialization

- Fix code style and documentation:
  - Update JSDoc comments
  - Fix string quotes consistency
  - Add proper error handling
  - Improve logging messages

- Update Vite config:
  - Add all Capacitor dependencies to external list
  - Ensure consistent handling across platforms
2025-04-08 08:06:00 +00:00
Matthew Raymer
1c8528fb20 feat: Improve database export with native sharing on mobile
- Enhance CapacitorPlatformService to use native share sheet via Web Share API
- Add fallback to Capacitor Share API with temporary file storage
- Implement WebPlatformService export with direct download for desktop
- Clean up temporary files and URLs after sharing/download

The changes provide a more platform-appropriate experience:
- Mobile: Uses system share sheet for flexible saving/sharing options
- Desktop: Direct download to user's download folder
2025-04-08 04:23:52 +00:00
Matthew Raymer
b8a7771edf feat(export): adapt DataExportSection for platform-specific file handling
Integrate PlatformServiceFactory to provide platform-specific data export:
- Add platform-specific file saving for Capacitor and other platforms
- Use web download mechanism only in web platform
- Conditionally show platform-specific save instructions
- Add iOS/Android detection for targeted guidance
- Update success messages based on platform context
- Improve download link visibility logic for web platform

This change ensures proper file handling across web, mobile, and desktop
platforms while maintaining a consistent user experience.
2025-04-07 08:50:09 +00:00
Matthew Raymer
5d845fb112 docs: add comprehensive JSDoc documentation to service layer
Add detailed TypeScript JSDoc documentation to core service modules:
- api.ts: Document error handling utilities and platform-specific logging
- plan.ts: Document plan/claim loading with retry mechanism
- deepLinks.ts: Document URL parsing and routing functionality
- Platform services:
  - CapacitorPlatformService: Document mobile platform capabilities
  - ElectronPlatformService: Document desktop placeholder implementation
  - PyWebViewPlatformService: Document Python bridge placeholder
  - WebPlatformService: Document web platform limitations and features

Key improvements:
- Add detailed @remarks sections explaining implementation details
- Include usage examples with TypeScript code snippets
- Document error handling and platform-specific behaviors
- Add @todo tags for unimplemented features
- Fix PlanResponse interface to include headers property

This documentation enhances code maintainability and developer experience
by providing clear guidance on service layer functionality and usage.
2025-04-07 07:49:39 +00:00
Matthew Raymer
660f2170de fix: improve error handling in photo upload
- Add proper unknown type for error handling in PhotoDialog
- Remove any type in favor of unknown for better type safety
- Fix error message access with type guards
2025-04-07 07:29:52 +00:00
Matthew Raymer
94bd649003 refactor: improve camera controls and modularize data export
- Add detailed error logging for image upload failures in PhotoDialog and SharedPhotoView
- Extract DataExportSection into standalone component with proper prop handling
- Fix Backup Identifier Seed visibility by passing activeDid prop
2025-04-07 07:17:43 +00:00
Matthew Raymer
b2d628cfeb chore: commit gitignore 2025-04-07 06:43:56 +00:00
Matthew Raymer
00e52f8dca feat: enhance error logging and upgrade Android build tools
- Add detailed error logging for image upload failures
- Upgrade Gradle to 8.11.1 and Android build tools to 8.9.0
- Add Capacitor camera and filesystem modules to Android build
2025-04-07 06:37:14 +00:00
Matthew Raymer
073ce24f43 chore(deps): Add Capacitor camera and filesystem plugins
- Add @capacitor/camera@6.0.0 for cross-platform photo capture
- Add @capacitor/filesystem@6.0.0 for file system operations
- Maintain compatibility with existing Capacitor core v6.2.1

These plugins enable native camera access and file system operations
for the Capacitor platform implementation.
2025-04-06 13:28:50 +00:00
Matthew Raymer
2c84bb50b3 **refactor(PhotoDialog, PlatformService): Implement cross-platform photo capture and encapsulated image processing**
- Replace direct camera library with platform-agnostic `PlatformService`
- Move platform-specific image processing logic to respective platform implementations
- Introduce `ImageResult` interface for consistent image handling across platforms
- Add support for native camera and image picker across all platforms
- Simplify `PhotoDialog` by removing platform-specific logic
- Maintain existing cropping and upload functionality
- Improve error handling and logging throughout
- Clean up UI for better user experience
- Add comprehensive documentation for usage and architecture

**BREAKING CHANGE:** Removes direct camera library dependency in favor of `PlatformService`

This change improves separation of concerns, enhances maintainability, and standardizes cross-platform image handling.
2025-04-06 13:04:26 +00:00
Matthew Raymer
abf18835f6 feat: update TypeScript config for platform services
- Add useDefineForClassFields for class field initialization
- Remove test-playwright from includes
- Add tsconfig.node.json reference
- Remove redundant node_modules exclude
2025-04-06 06:58:25 +00:00
Matthew Raymer
f72562804d feat: update TypeScript config for platform services
- Add useDefineForClassFields for class field initialization
- Remove test-playwright from includes
- Add tsconfig.node.json reference
- Remove redundant node_modules exclude
2025-04-06 06:58:14 +00:00
Matthew Raymer
bdc5ffafc1 baseline for this branch 2025-04-06 05:41:12 +00:00
68 changed files with 1791 additions and 496 deletions

View File

@@ -26,6 +26,7 @@ module.exports = {
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off"
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
},
};

6
.gitignore vendored
View File

@@ -51,5 +51,7 @@ vendor/
# Build logs
build_logs/
# PWA icon files generated by capacitor-assets
icons
android/app/src/main/assets/public
android/app/src/main/res
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
android/.gradle/file-system.probe

View File

@@ -187,11 +187,7 @@ Prerequisites: macOS with Xcode installed
3. Copy the assets:
```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
npx capacitor-assets generate --ios
```

5
android/.gitignore vendored
View File

@@ -102,8 +102,3 @@ app/src/main/assets/public
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
# Generated Icons from capacitor-assets
app/src/main/res/drawable/*.png
app/src/main/res/drawable-*/*.png
app/src/main/res/mipmap-*/*.png

Binary file not shown.

View File

@@ -10,6 +10,8 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-camera')
implementation project(':capacitor-filesystem')
}

View File

@@ -2,5 +2,13 @@
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
},
{
"pkg": "@capacitor/camera",
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
},
{
"pkg": "@capacitor/filesystem",
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
classpath 'com.android.tools.build:gradle:8.9.0'
classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -4,3 +4,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
include ':capacitor-camera'
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

BIN
assets/icon-only.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

4
ios/.gitignore vendored
View File

@@ -21,7 +21,3 @@ 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

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

View File

@@ -0,0 +1,23 @@
{
"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.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

22
ios/fastlane/Fastfile Normal file
View File

@@ -0,0 +1,22 @@
default_platform(:ios)
platform :ios do
desc "Build and deploy iOS app"
lane :beta do
build_ios_app(
scheme: "App",
workspace: "App.xcworkspace",
export_method: "app-store"
)
upload_to_testflight
end
lane :release do
build_ios_app(
scheme: "App",
workspace: "App.xcworkspace",
export_method: "app-store"
)
upload_to_app_store
end
end

229
package-lock.json generated
View File

@@ -10,8 +10,10 @@
"dependencies": {
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0",
"@capacitor/camera": "^6.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
@@ -2768,6 +2770,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@capacitor/camera": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-6.0.0.tgz",
"integrity": "sha512-AZ/gfVPC3lsKbk9/yHI60ygNyOkN5jsCb4bHxXFbW0bss3XYtR/J1XWFJGkFNiRErNnTz6jnDrhsGCr7+JPfiA==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capacitor/cli": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-6.2.1.tgz",
@@ -2878,6 +2889,15 @@
"tslib": "^2.1.0"
}
},
"node_modules/@capacitor/filesystem": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-6.0.0.tgz",
"integrity": "sha512-GnC4CBfky7fvG9zSV/aQnZaGs6ZJ90AaQorr53z81ArTCqcrSUeBMuCxWmvti9HrdXLhBavyA1UOjvRGObOFjg==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capacitor/ios": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
@@ -5181,9 +5201,9 @@
}
},
"node_modules/@expo/cli": {
"version": "0.22.23",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.23.tgz",
"integrity": "sha512-LXFKu2jnk9ClVD+kw0sJCQ89zei01wz2t4EJwc9P7EwYb8gabC8FtPyM/X7NIE5jtrnTLTUtjW5ovxQSBL7pJQ==",
"version": "0.22.24",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.24.tgz",
"integrity": "sha512-lhdenxBC8/x/vL39j79eXE09mOaqNNLmiSDdY/PblnI+UNzGgsQ48hBTYa/MQhd0ioXXVKurZL2941dLKwcxJw==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -5201,12 +5221,12 @@
"@expo/osascript": "^2.1.6",
"@expo/package-manager": "^1.7.2",
"@expo/plist": "^0.2.2",
"@expo/prebuild-config": "^8.0.30",
"@expo/prebuild-config": "^8.0.31",
"@expo/rudder-sdk-node": "^1.1.1",
"@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0",
"@react-native/dev-middleware": "0.76.8",
"@react-native/dev-middleware": "0.76.9",
"@urql/core": "^5.0.6",
"@urql/exchange-retry": "^1.3.0",
"accepts": "^1.3.8",
@@ -6602,9 +6622,9 @@
}
},
"node_modules/@expo/prebuild-config": {
"version": "8.0.30",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.30.tgz",
"integrity": "sha512-xNHWGh0xLZjxBXwVbDW+TPeexuQ95FZX2ZRrzJkALxhQiwYQswQSFE7CVUFMC2USIKVklCcgfEvtqnguTBQVxQ==",
"version": "8.0.31",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.31.tgz",
"integrity": "sha512-YTuS5ic9KolD/WA3GqgLcZytHQU1dpitlZ/cbDq8ZqkY+1ae5YWX+GkYEZf2VyECPaWnHYuDGddaTQVw5miTRg==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -6614,7 +6634,7 @@
"@expo/config-types": "^52.0.5",
"@expo/image-utils": "^0.6.5",
"@expo/json-file": "^9.0.2",
"@react-native/normalize-colors": "0.76.8",
"@react-native/normalize-colors": "0.76.9",
"debug": "^4.3.1",
"fs-extra": "^9.0.0",
"resolve-from": "^5.0.0",
@@ -8010,23 +8030,23 @@
}
},
"node_modules/@react-native/babel-plugin-codegen": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.8.tgz",
"integrity": "sha512-84RUEhDZS+q7vPtxKi0iMZLd5/W0VN7NOyqX5f+burV3xMYpUhpF5TDJ2Ysol7dJrvEZHm6ISAriO85++V8YDw==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.9.tgz",
"integrity": "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@react-native/codegen": "0.76.8"
"@react-native/codegen": "0.76.9"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-preset": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.8.tgz",
"integrity": "sha512-xrP+r3orRzzxtC2TrfGIP6IYi1f4AiWlnSiWf4zxEdMFzKrYdmxhD0FPtAZb77B0DqFIW5AcBFlm4grfL/VgfA==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.9.tgz",
"integrity": "sha512-TbSeCplCM6WhL3hR2MjC/E1a9cRnMLz7i767T7mP90oWkklEjyPxWl+0GGoVGnJ8FC/jLUupg/HvREKjjif6lw==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8072,7 +8092,7 @@
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
"@react-native/babel-plugin-codegen": "0.76.8",
"@react-native/babel-plugin-codegen": "0.76.9",
"babel-plugin-syntax-hermes-parser": "^0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
@@ -8085,9 +8105,9 @@
}
},
"node_modules/@react-native/codegen": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.8.tgz",
"integrity": "sha512-qvKhcYBkRHJFkeWrYm66kEomQOTVXWiHBkZ8VF9oC/71OJkLszpTpVOuPIyyib6fqhjy9l7mHYGYenSpfYI5Ww==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.9.tgz",
"integrity": "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8224,9 +8244,9 @@
}
},
"node_modules/@react-native/debugger-frontend": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.8.tgz",
"integrity": "sha512-kSukBw2C++5ENLUCAp/1uEeiFgiHi/MBa71Wgym3UD5qwu2vOSPOTSKRX7q2Jb676MUzTcrIaJBZ/r2qk25u7Q==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.9.tgz",
"integrity": "sha512-0Ru72Bm066xmxFuOXhhvrryxvb57uI79yDSFf+hxRpktkC98NMuRenlJhslMrbJ6WjCu1vOe/9UjWNYyxXTRTA==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
@@ -8235,15 +8255,15 @@
}
},
"node_modules/@react-native/dev-middleware": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.8.tgz",
"integrity": "sha512-KYx7hFME2uYQRCDCqb19ghw51TAdh48PZ5EMpoU2kPA1SKKO9c1bUbpsKRhVZ0bv1QqEX6fjox3c4/WYRozHQA==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.9.tgz",
"integrity": "sha512-xkd3C3dRcmZLjFTEAOvC14q3apMLouIvJViCZY/p1EfCMrNND31dgE1dYrLTiI045WAWMt5bD15i6f7dE2/QWA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.76.8",
"@react-native/debugger-frontend": "0.76.9",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@@ -8571,9 +8591,9 @@
}
},
"node_modules/@react-native/normalize-colors": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.8.tgz",
"integrity": "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.9.tgz",
"integrity": "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw==",
"license": "MIT",
"optional": true,
"peer": true
@@ -9757,9 +9777,9 @@
}
},
"node_modules/@types/babel__generator": {
"version": "7.6.8",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -9946,9 +9966,9 @@
}
},
"node_modules/@types/luxon": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.0.tgz",
"integrity": "sha512-RtEj20xRyG7cRp142MkQpV3GRF8Wo2MtDkKLz65MQs7rM1Lh8bz+HtfPXCCJEYpnDFu6VwAq/Iv2Ikyp9Jw/hw==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
"dev": true,
"license": "MIT"
},
@@ -12320,9 +12340,9 @@
}
},
"node_modules/babel-preset-expo": {
"version": "12.0.10",
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.10.tgz",
"integrity": "sha512-6QE52Bxsp5XRE8t0taKRFTFsmTG0ThQE+PTgCgLY9s8v2Aeh8R+E+riXhSHX6hP+diDmBFBdvLCUTq7kroJb1Q==",
"version": "12.0.11",
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.11.tgz",
"integrity": "sha512-4m6D92nKEieg+7DXa8uSvpr0GjfuRfM/G0t0I/Q5hF8HleEv5ms3z4dJ+p52qXSJsm760tMqLdO93Ywuoi7cCQ==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -12333,7 +12353,7 @@
"@babel/plugin-transform-parameters": "^7.22.15",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0",
"@react-native/babel-preset": "0.76.8",
"@react-native/babel-preset": "0.76.9",
"babel-plugin-react-native-web": "~0.19.13",
"react-refresh": "^0.14.2"
},
@@ -12393,9 +12413,9 @@
"optional": true
},
"node_modules/bare-fs": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
"integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz",
"integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
@@ -12547,9 +12567,9 @@
}
},
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.0.tgz",
"integrity": "sha512-JocpCSOixzy5XFJi2ub6IMmV/G9i8Lrm2lZvwBv9xPdglmZM0ufDVBbjbrfU/zuLvBfD7Bv2eYxz9i+OHTgkew==",
"license": "MIT",
"engines": {
"node": "*"
@@ -13193,9 +13213,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
"version": "1.0.30001712",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001712.tgz",
"integrity": "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==",
"devOptional": true,
"funding": [
{
@@ -13947,9 +13967,9 @@
}
},
"node_modules/config-file-ts/node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -15601,9 +15621,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.129",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz",
"integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==",
"version": "1.5.132",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.132.tgz",
"integrity": "sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==",
"devOptional": true,
"license": "ISC"
},
@@ -16027,14 +16047,14 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz",
"integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==",
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.10.2"
"synckit": "^0.11.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -16642,21 +16662,21 @@
}
},
"node_modules/expo": {
"version": "52.0.42",
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.42.tgz",
"integrity": "sha512-t+PRYIzzPFAlF99OVJOjZwM1glLhN85XGD6vmeg6uwpADDILl9yw4dfy0DXL4hot5GJkAGaZ+uOHUljV4kC2Bg==",
"version": "52.0.44",
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.44.tgz",
"integrity": "sha512-qj3+MWxmqLyBaYQ8jDOvVLEgSqNplH3cf+nDhxCo4C1cpTPD1u/HGh1foibtaeuCYLHsE5km1lrcOpRbFJ4luQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.22.23",
"@expo/cli": "0.22.24",
"@expo/config": "~10.0.11",
"@expo/config-plugins": "~9.0.17",
"@expo/fingerprint": "0.11.11",
"@expo/metro-config": "0.19.12",
"@expo/vector-icons": "^14.0.0",
"babel-preset-expo": "~12.0.10",
"babel-preset-expo": "~12.0.11",
"expo-asset": "~11.0.5",
"expo-constants": "~17.0.8",
"expo-file-system": "~18.0.12",
@@ -18566,9 +18586,9 @@
}
},
"node_modules/image-size": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz",
"integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -23524,9 +23544,9 @@
}
},
"node_modules/nostr-tools": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.1.tgz",
"integrity": "sha512-+Oj5t+behIkU9kh3go5wg8Aa5oR7euBU9gOItUNapJe5Gaa+KPzMuTIN+rMRK3DaZ4Zt6RM4kR/ddwstzGKf7g==",
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
@@ -28790,9 +28810,9 @@
}
},
"node_modules/synckit": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz",
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==",
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.2.tgz",
"integrity": "sha512-1IUffI8zZ8qUMB3NUJIjk0RpLroG/8NkQDAWH1NbB2iJ0/5pn3M8rxfNzMz4GH9OnYaGYn31LEDSXJp/qIlxgA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -28803,7 +28823,7 @@
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
"url": "https://opencollective.com/synckit"
}
},
"node_modules/synckit/node_modules/tslib": {
@@ -29614,24 +29634,24 @@
}
},
"node_modules/typeorm": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.21.tgz",
"integrity": "sha512-lh4rUWl1liZGjyPTWpwcK8RNI5x4ekln+/JJOox1wCd7xbucYDOXWD+1cSzTN3L0wbTGxxOtloM5JlxbOxEufA==",
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.22.tgz",
"integrity": "sha512-P/Tsz3UpJ9+K0oryC0twK5PO27zejLYYwMsE8SISfZc1lVHX+ajigiOyWsKbuXpEFMjD9z7UjLzY3+ElVOMMDA==",
"license": "MIT",
"dependencies": {
"@sqltools/formatter": "^1.2.5",
"ansis": "^3.9.0",
"ansis": "^3.17.0",
"app-root-path": "^3.1.0",
"buffer": "^6.0.3",
"dayjs": "^1.11.9",
"debug": "^4.3.4",
"dotenv": "^16.0.3",
"dayjs": "^1.11.13",
"debug": "^4.4.0",
"dotenv": "^16.4.7",
"glob": "^10.4.5",
"sha.js": "^2.4.11",
"sql-highlight": "^6.0.0",
"tslib": "^2.5.0",
"uuid": "^11.0.5",
"yargs": "^17.6.2"
"tslib": "^2.8.1",
"uuid": "^11.1.0",
"yargs": "^17.7.2"
},
"bin": {
"typeorm": "cli.js",
@@ -29645,12 +29665,12 @@
"url": "https://opencollective.com/typeorm"
},
"peerDependencies": {
"@google-cloud/spanner": "^5.18.0",
"@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0",
"@sap/hana-client": "^2.12.25",
"better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"hdb-pool": "^0.1.6",
"ioredis": "^5.0.4",
"mongodb": "^5.8.0",
"mongodb": "^5.8.0 || ^6.0.0",
"mssql": "^9.1.1 || ^10.0.1 || ^11.0.1",
"mysql2": "^2.2.5 || ^3.0.1",
"oracledb": "^6.3.0",
@@ -29662,7 +29682,7 @@
"sql.js": "^1.4.0",
"sqlite3": "^5.0.3",
"ts-node": "^10.7.0",
"typeorm-aurora-data-api-driver": "^2.0.0"
"typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0"
},
"peerDependenciesMeta": {
"@google-cloud/spanner": {
@@ -29718,6 +29738,23 @@
}
}
},
"node_modules/typeorm/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/typeorm/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -29753,6 +29790,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/typeorm/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/typeorm/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typeorm/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@@ -30206,9 +30255,9 @@
}
},
"node_modules/vite": {
"version": "5.4.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
"version": "5.4.17",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz",
"integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -44,8 +44,10 @@
"dependencies": {
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0",
"@capacitor/camera": "^6.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",

View File

@@ -0,0 +1,193 @@
/** * Data Export Section Component * * Provides UI and functionality for
exporting user data and backing up identifier seeds. * Includes buttons for seed
backup and database export, with platform-specific download instructions. * *
@component * @displayName DataExportSection * @example * ```vue *
<DataExportSection :active-did="currentDid" />
* ``` */
<template>
<div
id="sectionDataExport"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
>
<div class="mb-2 font-bold">Data Export</div>
<router-link
v-if="activeDid"
:to="{ name: 'seed-backup' }"
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 mb-2 mt-2"
>
Backup Identifier Seed
</router-link>
<button
:class="computedStartDownloadLinkClassNames()"
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()"
>
Download Settings & Contacts
<br />
(excluding Identifier Data)
</button>
<a
v-if="platformService?.needsSecondaryDownloadLink()"
ref="downloadLink"
:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
</a>
<div
v-if="platformService?.getExportInstructions().length > 0"
class="mt-4"
>
<p
v-for="instruction in platformService?.getExportInstructions()"
:key="instruction"
class="list-disc list-outside ml-4"
>
{{ instruction }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { PlatformService } from "../services/PlatformService";
/**
* @vue-component
* Data Export Section Component
* Handles database export and seed backup functionality with platform-specific behavior
*/
@Component
export default class DataExportSection extends Vue {
/**
* Notification function injected by Vue
* Used to show success/error messages to the user
*/
$notify!: (notification: NotificationIface, timeout?: number) => void;
/**
* Active DID (Decentralized Identifier) of the user
* Controls visibility of seed backup option
* @required
*/
@Prop({ required: true }) readonly activeDid!: string;
/**
* URL for the database export download
* Created and revoked dynamically during export process
* Only used in web platform
*/
downloadUrl = "";
/**
* Platform service instance for platform-specific operations
*/
private platformService?: PlatformService;
/**
* Lifecycle hook to clean up resources
* Revokes object URL when component is unmounted (web platform only)
*/
beforeUnmount() {
if (this.downloadUrl && this.platformService?.needsDownloadCleanup()) {
URL.revokeObjectURL(this.downloadUrl);
}
}
async mounted() {
this.platformService = await PlatformServiceFactory.getInstance();
logger.log(
"DataExportSection mounted on platform:",
process.env.VITE_PLATFORM,
);
}
/**
* Exports the database to a JSON file
* Uses platform-specific methods for saving the exported data
* Shows success/error notifications to user
*
* @throws {Error} If export fails
* @emits {Notification} Success or error notification
*/
public async exportDatabase() {
try {
if (!this.platformService) {
this.platformService = await PlatformServiceFactory.getInstance();
}
logger.log(
"Starting database export on platform:",
process.env.VITE_PLATFORM,
);
const blob = await db.export({ prettyJson: true });
const fileName = `${db.name}-backup.json`;
logger.log("Database export details:", {
fileName,
blobSize: `${blob.size} bytes`,
platform: process.env.VITE_PLATFORM,
});
await this.platformService.exportDatabase(blob, fileName);
logger.log("Database export completed successfully:", {
fileName,
platform: process.env.VITE_PLATFORM,
});
this.$notify(
{
group: "alert",
type: "success",
title: "Export Successful",
text: this.platformService.getExportSuccessMessage(),
},
-1,
);
} catch (error) {
logger.error("Database export failed:", {
error,
platform: process.env.VITE_PLATFORM,
});
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "There was an error exporting the data.",
},
3000,
);
}
}
/**
* Computes class names for the initial download button
* @returns Object with 'hidden' class when download is in progress (web platform only)
*/
public computedStartDownloadLinkClassNames() {
return {
hidden:
this.downloadUrl && this.platformService?.needsSecondaryDownloadLink(),
};
}
/**
* Computes class names for the secondary download link
* @returns Object with 'hidden' class when no download is available or not on web platform
*/
public computedDownloadLinkClassNames() {
return {
hidden:
!this.downloadUrl ||
!this.platformService?.needsSecondaryDownloadLink(),
};
}
}
</script>

View File

@@ -40,11 +40,6 @@
}"
class="max-h-[90vh] max-w-[90vw] object-contain"
/>
<!-- This gives a round cropper.
:presetMode="{
mode: 'round',
}"
-->
</div>
<div v-else>
<div class="flex justify-center">
@@ -74,95 +69,80 @@
</button>
</div>
</div>
<div v-else ref="cameraContainer">
<!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result,
eg. the following which just stretches it vertically:
:resolution="{ width: 375, height: 812 }"
-->
<camera
ref="camera"
facing-mode="environment"
autoplay
@started="cameraStarted()"
>
<div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
<div v-else>
<div class="flex flex-col items-center justify-center gap-4 p-4">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="takePhoto"
>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="takeImage()"
>
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
</button>
</div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
<font-awesome icon="camera" class="w-[1em]" />
</button>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="pickPhoto"
>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="swapMirrorClass()"
>
<font-awesome icon="left-right" class="w-[1em]"></font-awesome>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="switchCamera()"
>
<font-awesome icon="rotate" class="w-[1em]"></font-awesome>
</button>
</div>
</camera>
<font-awesome icon="image" class="w-[1em]" />
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
/**
* PhotoDialog.vue - Cross-platform photo capture and selection component
*
* This component provides a unified interface for taking photos and selecting images
* across different platforms using the PlatformService.
*
* @author Matthew Raymer
* @file PhotoDialog.vue
*/
import axios from "axios";
import Camera from "simple-vue-camera";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { PlatformService } from "../services/PlatformService";
@Component({ components: { Camera, VuePictureCropper } })
@Component({ components: { VuePictureCropper } })
export default class PhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0;
activeDid = "";
blob?: Blob;
claimType = "";
crop = false;
fileName?: string;
mirror = false;
numDevices = 0;
setImageCallback: (arg: string) => void = () => {};
showRetry = true;
uploading = false;
visible = false;
private platformService?: PlatformService;
URL = window.URL || window.webkitURL;
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.platformService = await PlatformServiceFactory.getInstance();
} catch (err: unknown) {
logger.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
text:
err instanceof Error
? err.message
: "There was an error retrieving your settings.",
},
-1,
);
@@ -173,7 +153,7 @@ export default class PhotoDialog extends Vue {
setImageFn: (arg: string) => void,
claimType: string,
crop?: boolean,
blob?: Blob, // for image upload, just to use the cropping function
blob?: Blob,
inputFileName?: string,
) {
this.visible = true;
@@ -187,7 +167,6 @@ export default class PhotoDialog extends Vue {
if (blob) {
this.blob = blob;
this.fileName = inputFileName;
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
this.showRetry = false;
} else {
this.blob = undefined;
@@ -205,85 +184,47 @@ export default class PhotoDialog extends Vue {
this.blob = undefined;
}
async cameraStarted() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
if (cameraComponent) {
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
this.mirror = cameraComponent.facingMode === "user";
// figure out which device is active
const currentDeviceId = cameraComponent.currentDeviceID();
const devices = await cameraComponent.devices(["videoinput"]);
this.activeDeviceNumber = devices.findIndex(
(device) => device.deviceId === currentDeviceId,
);
}
}
async switchCamera() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
const devices = await cameraComponent?.devices(["videoinput"]);
await cameraComponent?.changeCamera(
devices[this.activeDeviceNumber].deviceId,
);
}
async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
/**
* This logic to set the image height & width correctly.
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
* Now that I've done it, I can't explain why it works.
*/
let imageHeight = cameraComponent?.resolution?.height;
let imageWidth = cameraComponent?.resolution?.width;
const initialImageRatio = imageWidth / imageHeight;
const windowRatio = window.innerWidth / window.innerHeight;
if (initialImageRatio > 1 && windowRatio < 1) {
// the image is wider than it is tall, and the window is taller than it is wide
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
// We're gonna force it opposite.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
} else if (initialImageRatio < 1 && windowRatio > 1) {
// the image is taller than it is wide, and the window is wider than it is tall
// Haven't seen this happen, but we'll do it just in case.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
}
const newImageRatio = imageWidth / imageHeight;
if (newImageRatio < windowRatio) {
// the image is a taller ratio than the window, so fit the height first
imageHeight = window.innerHeight / 2;
imageWidth = imageHeight * newImageRatio;
} else {
// the image is a wider ratio than the window, so fit the width first
imageWidth = window.innerWidth / 2;
imageHeight = imageWidth / newImageRatio;
}
// The resolution is only necessary because of that mobile portrait-orientation case.
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
this.blob =
(await cameraComponent?.snapshot({
height: imageHeight,
width: imageWidth,
})) || undefined;
// png is default
this.fileName = "snapshot.png";
if (!this.blob) {
async takePhoto() {
try {
if (!this.platformService) {
this.platformService = await PlatformServiceFactory.getInstance();
}
const result = await this.platformService.takePicture();
this.blob = result.blob;
this.fileName = result.fileName;
} catch (error: unknown) {
logger.error("Error taking picture:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error taking the picture. Please try again.",
text: "Failed to take picture. Please try again.",
},
5000,
);
}
}
async pickPhoto() {
try {
if (!this.platformService) {
this.platformService = await PlatformServiceFactory.getInstance();
}
const result = await this.platformService.pickImage();
this.blob = result.blob;
this.fileName = result.fileName;
} catch (error: unknown) {
logger.error("Error picking image:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to pick image. Please try again.",
},
5000,
);
return;
}
}
@@ -295,51 +236,6 @@ export default class PhotoDialog extends Vue {
this.blob = undefined;
}
/****
Here's an approach to photo capture without a library. It has similar quirks.
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
<button id="start-camera" @click="cameraClicked">Start Camera</button>
<video id="video" width="320" height="240" autoplay></video>
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
<canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() {
const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
if (video instanceof HTMLVideoElement) {
video.srcObject = stream;
}
}
photoSnapped() {
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
if (
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
canvas
?.getContext("2d")
?.drawImage(video, 0, 0, canvas.width, canvas.height);
// ... or set the blob:
// canvas?.toBlob(
// (blob) => {
// this.blob = blob;
// },
// "image/jpeg",
// 1,
// );
// data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg");
}
}
****/
async uploadImage() {
this.uploading = true;
@@ -350,11 +246,9 @@ export default class PhotoDialog extends Vue {
const token = await accessToken(this.activeDid);
const headers = {
Authorization: "Bearer " + token,
// axios fills in Content-Type of multipart/form-data
};
const formData = new FormData();
if (!this.blob) {
// yeah, this should never happen, but it helps with subsequent type checking
this.$notify(
{
group: "alert",
@@ -367,7 +261,7 @@ export default class PhotoDialog extends Vue {
this.uploading = false;
return;
}
formData.append("image", this.blob, this.fileName || "snapshot.png");
formData.append("image", this.blob, this.fileName || "photo.jpg");
formData.append("claimType", this.claimType);
try {
if (
@@ -387,14 +281,64 @@ export default class PhotoDialog extends Vue {
this.close();
this.setImageCallback(response.data.url as string);
} catch (error) {
logger.error("Error uploading the image", error);
} catch (error: unknown) {
// Log the raw error first
logger.error("Raw error object:", JSON.stringify(error, null, 2));
let errorMessage = "There was an error saving the picture.";
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const statusText = error.response?.statusText;
const data = error.response?.data;
// Log detailed error information
logger.error("Upload error details:", {
status,
statusText,
data: JSON.stringify(data, null, 2),
message: error.message,
config: {
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers,
},
});
if (status === 401) {
errorMessage = "Authentication failed. Please try logging in again.";
} else if (status === 413) {
errorMessage = "Image file is too large. Please try a smaller image.";
} else if (status === 415) {
errorMessage =
"Unsupported image format. Please try a different image.";
} else if (status && status >= 500) {
errorMessage = "Server error. Please try again later.";
} else if (data?.message) {
errorMessage = data.message;
}
} else if (error instanceof Error) {
// Log non-Axios error with full details
logger.error("Non-Axios error details:", {
name: error.name,
message: error.message,
stack: error.stack,
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
});
} else {
// Log any other type of error
logger.error("Unknown error type:", {
error: JSON.stringify(error, null, 2),
type: typeof error,
});
}
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error saving the picture.",
text: errorMessage,
},
5000,
);
@@ -402,17 +346,6 @@ export default class PhotoDialog extends Vue {
this.blob = undefined;
}
}
swapMirrorClass() {
this.mirror = !this.mirror;
if (this.mirror) {
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
} else {
(this.$refs.cameraContainer as HTMLElement).classList.remove(
"mirror-video",
);
}
}
}
</script>
@@ -438,12 +371,4 @@ export default class PhotoDialog extends Vue {
width: 100%;
max-width: 700px;
}
.mirror-video {
transform: scaleX(-1);
-webkit-transform: scaleX(-1); /* For Safari */
-moz-transform: scaleX(-1); /* For Firefox */
-ms-transform: scaleX(-1); /* For IE */
-o-transform: scaleX(-1); /* For Opera */
}
</style>

View File

@@ -1,101 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Registration Needed</h1>
Before you can perform certain actions in the app, you need to register an account. It's easy, and it's FREE!
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
type="button"
class="block w-full text-center text-lg font-bold 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-2 py-3 rounded-md mb-2"
@click="onClickSaveChanges()"
>
Register
</button>
<button
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="onClickCancel()"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
@Component
export default class RegistrationGate extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (text: string, expiresAt: string) => void = () => {};
inviteIdentifier = "";
text = "";
visible = false;
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
.toISOString()
.substring(0, 10);
async open(
inviteIdentifier: string,
aCallback: (text: string, expiresAt: string) => void,
) {
this.callback = aCallback;
this.inviteIdentifier = inviteIdentifier;
this.visible = true;
}
async onClickSaveChanges() {
if (!this.expiresAt) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Needs Expiration",
text: "You must select an expiration date.",
},
5000,
);
} else {
this.callback(this.text, this.expiresAt);
this.visible = false;
}
}
onClickCancel() {
this.visible = false;
}
}
</script>
<style>
.dialog-overlay {
z-index: 50;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>

View File

@@ -0,0 +1,127 @@
/**
* Represents the result of an image capture or selection operation.
* Contains both the image data as a Blob and the associated filename.
*/
export interface ImageResult {
/** The image data as a Blob object */
blob: Blob;
/** The filename associated with the image */
fileName: string;
}
/**
* Platform-agnostic interface for handling platform-specific operations.
* Provides a common API for file system operations, camera interactions,
* platform detection, and deep linking across different platforms
* (web, mobile, desktop).
*/
export interface PlatformService {
// File system operations
/**
* Reads the contents of a file at the specified path.
* @param path - The path to the file to read
* @returns Promise resolving to the file contents as a string
*/
readFile(path: string): Promise<string>;
/**
* Writes content to a file at the specified path.
* @param path - The path where the file should be written
* @param content - The content to write to the file
* @returns Promise that resolves when the write is complete
*/
writeFile(path: string, content: string): Promise<void>;
/**
* Deletes a file at the specified path.
* @param path - The path to the file to delete
* @returns Promise that resolves when the deletion is complete
*/
deleteFile(path: string): Promise<void>;
/**
* Lists all files in the specified directory.
* @param directory - The directory path to list
* @returns Promise resolving to an array of filenames
*/
listFiles(directory: string): Promise<string[]>;
/**
* Exports a database blob to a file, handling platform-specific save operations.
* @param blob - The database blob to export
* @param fileName - The name of the file to save
* @returns Promise that resolves when the export is complete
* @throws Error if export fails
*/
exportDatabase(blob: Blob, fileName: string): Promise<void>;
// Camera operations
/**
* Activates the device camera to take a picture.
* @returns Promise resolving to the captured image result
*/
takePicture(): Promise<ImageResult>;
/**
* Opens a file picker to select an existing image.
* @returns Promise resolving to the selected image result
*/
pickImage(): Promise<ImageResult>;
// Platform specific features
/**
* Checks if the current platform is Capacitor (mobile).
* @returns true if running on Capacitor
*/
isCapacitor(): boolean;
/**
* Checks if the current platform is Electron (desktop).
* @returns true if running on Electron
*/
isElectron(): boolean;
/**
* Checks if the current platform is PyWebView.
* @returns true if running on PyWebView
*/
isPyWebView(): boolean;
/**
* Checks if the current platform is web browser.
* @returns true if running in a web browser
*/
isWeb(): boolean;
// Deep linking
/**
* Handles deep link URLs for the application.
* @param url - The deep link URL to handle
* @returns Promise that resolves when the deep link has been handled
*/
handleDeepLink(url: string): Promise<void>;
/**
* Gets platform-specific instructions for saving exported files
* @returns Array of instruction strings for the current platform
*/
getExportInstructions(): string[];
/**
* Gets the success message for database export
* @returns Success message appropriate for the current platform
*/
getExportSuccessMessage(): string;
/**
* Checks if the platform requires a secondary download link
* @returns true if platform needs a secondary download link
*/
needsSecondaryDownloadLink(): boolean;
/**
* Checks if the platform needs cleanup after download
* @returns true if platform needs cleanup after download
*/
needsDownloadCleanup(): boolean;
}

View File

@@ -0,0 +1,69 @@
import { PlatformService } from "./PlatformService";
import { WebPlatformService } from "./platforms/WebPlatformService";
/**
* Factory class for creating platform-specific service implementations.
* Implements the Singleton pattern to ensure only one instance of PlatformService exists.
*
* The factory determines which platform implementation to use based on the VITE_PLATFORM
* environment variable. Supported platforms are:
* - capacitor: Mobile platform using Capacitor
* - electron: Desktop platform using Electron
* - pywebview: Python WebView implementation
* - web: Default web platform (fallback)
*
* @example
* ```typescript
* const platformService = PlatformServiceFactory.getInstance();
* await platformService.takePicture();
* ```
*/
export class PlatformServiceFactory {
private static instance: PlatformService | null = null;
/**
* Gets or creates the singleton instance of PlatformService.
* Creates the appropriate platform-specific implementation based on environment.
*
* @returns {PlatformService} The singleton instance of PlatformService
*/
public static async getInstance(): Promise<PlatformService> {
if (PlatformServiceFactory.instance) {
return PlatformServiceFactory.instance;
}
const platform = process.env.VITE_PLATFORM || "web";
let service: PlatformService;
switch (platform) {
case "capacitor": {
const { CapacitorPlatformService } = await import(
"./platforms/CapacitorPlatformService"
);
service = new CapacitorPlatformService();
break;
}
case "electron": {
const { ElectronPlatformService } = await import(
"./platforms/ElectronPlatformService"
);
service = new ElectronPlatformService();
break;
}
case "pywebview": {
const { PyWebViewPlatformService } = await import(
"./platforms/PyWebViewPlatformService"
);
service = new PyWebViewPlatformService();
break;
}
case "web":
default:
service = new WebPlatformService();
break;
}
PlatformServiceFactory.instance = service;
return service;
}
}

View File

@@ -1,5 +1,40 @@
/**
* API error handling utilities for the application.
* Provides centralized error handling for API requests with platform-specific logging.
*
* @module api
*/
import { AxiosError } from "axios";
import { logger } from "../utils/logger";
/**
* Handles API errors with platform-specific logging and error processing.
*
* @param error - The Axios error object from the failed request
* @param endpoint - The API endpoint that was called
* @returns null for rate limit errors (400), throws the error otherwise
* @throws The original error for non-rate-limit cases
*
* @remarks
* Special handling includes:
* - Enhanced logging for Capacitor platform
* - Rate limit detection and handling
* - Detailed error information logging including:
* - Error message
* - HTTP status
* - Response data
* - Request configuration (URL, method, headers)
*
* @example
* ```typescript
* try {
* await api.getData();
* } catch (error) {
* handleApiError(error as AxiosError, '/api/data');
* }
* ```
*/
export const handleApiError = (error: AxiosError, endpoint: string) => {
if (process.env.VITE_PLATFORM === "capacitor") {
logger.error(`[Capacitor API Error] ${endpoint}:`, {

View File

@@ -23,6 +23,23 @@
* - Query parameter validation and sanitization
* - Type-safe parameter passing to router
*
* Deep Link Format:
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
*
* Supported Routes:
* - user-profile: View user profile
* - project-details: View project details
* - onboard-meeting-setup: Setup onboarding meeting
* - invite-one-accept: Accept invitation
* - contact-import: Import contacts
* - confirm-gift: Confirm gift
* - claim: View claim
* - claim-cert: View claim certificate
* - claim-add-raw: Add raw claim
* - contact-edit: Edit contact
* - contacts: View contacts
* - did: View DID
*
* @example
* const handler = new DeepLinkHandler(router);
* await handler.handleDeepLink("timesafari://claim/123?view=details");
@@ -38,15 +55,28 @@ import {
import { logConsoleAndDb } from "../db";
import type { DeepLinkError } from "../interfaces/deepLinks";
/**
* Handles processing and routing of deep links in the application.
* Provides validation, error handling, and routing for deep link URLs.
*/
export class DeepLinkHandler {
private router: Router;
/**
* Creates a new DeepLinkHandler instance.
* @param router - Vue Router instance for navigation
*/
constructor(router: Router) {
this.router = router;
}
/**
* Parses deep link URL into path, params and query components
* Parses deep link URL into path, params and query components.
* Validates URL structure using Zod schemas.
*
* @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid
* @returns Parsed URL components (path, params, query)
*/
private parseDeepLink(url: string) {
const parts = url.split("://");
@@ -79,8 +109,11 @@ export class DeepLinkHandler {
}
/**
* Processes incoming deep links and routes them appropriately
* @param url The deep link URL to process
* Processes incoming deep links and routes them appropriately.
* Handles validation, error handling, and routing to the correct view.
*
* @param url - The deep link URL to process
* @throws {DeepLinkError} If URL processing fails
*/
async handleDeepLink(url: string): Promise<void> {
try {
@@ -107,7 +140,13 @@ export class DeepLinkHandler {
}
/**
* Routes the deep link to appropriate view with validated parameters
* Routes the deep link to appropriate view with validated parameters.
* Validates route and parameters using Zod schemas before routing.
*
* @param path - The route path from the deep link
* @param params - URL parameters
* @param query - Query string parameters
* @throws {DeepLinkError} If validation fails or route is invalid
*/
private async validateAndRoute(
path: string,

View File

@@ -1,12 +1,55 @@
/**
* Plan service module for handling plan and claim data loading.
* Provides functionality to load plans with retry mechanism and error handling.
*
* @module plan
*/
import axios from "axios";
import { logger } from "../utils/logger";
/**
* Response interface for plan loading operations.
* Represents the structure of both successful and error responses.
*/
interface PlanResponse {
/** The response data payload */
data?: unknown;
/** HTTP status code of the response */
status?: number;
/** Error message in case of failure */
error?: string;
/** Response headers */
headers?: unknown;
}
/**
* Loads a plan with automatic retry mechanism.
* Attempts to load the plan multiple times in case of failure.
*
* @param handle - The unique identifier for the plan or claim
* @param retries - Number of retry attempts (default: 3)
* @returns Promise resolving to PlanResponse
*
* @remarks
* - Implements exponential backoff with 1 second delay between retries
* - Provides detailed logging of each attempt and any errors
* - Handles both plan and claim flows based on handle content
* - Logs comprehensive error information including:
* - HTTP status and headers
* - Response data
* - Request configuration
*
* @example
* ```typescript
* const response = await loadPlanWithRetry('plan-123');
* if (response.error) {
* console.error(response.error);
* } else {
* console.log(response.data);
* }
* ```
*/
export const loadPlanWithRetry = async (
handle: string,
retries = 3,
@@ -58,6 +101,22 @@ export const loadPlanWithRetry = async (
}
};
/**
* Makes a single API request to load a plan or claim.
* Determines the appropriate endpoint based on the handle.
*
* @param handle - The unique identifier for the plan or claim
* @returns Promise resolving to PlanResponse
* @throws Will throw an error if the API request fails
*
* @remarks
* - Automatically detects claim vs plan endpoints based on handle
* - Uses axios for HTTP requests
* - Provides detailed error logging
* - Different endpoints:
* - Claims: /api/claims/{handle}
* - Plans: /api/plans/{handle}
*/
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
logger.log(`[Plan Service] Making API request for plan ${handle}`);

View File

@@ -0,0 +1,281 @@
import { ImageResult, PlatformService } from "../PlatformService";
import { Filesystem, Directory } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { logger } from "../../utils/logger";
import { Share } from "@capacitor/share";
/**
* Platform service implementation for Capacitor (mobile) platform.
* Provides native mobile functionality through Capacitor plugins for:
* - File system operations
* - Camera and image picker
* - Platform-specific features
*/
export class CapacitorPlatformService implements PlatformService {
/**
* Reads a file from the app's data directory.
* @param path - Relative path to the file in the app's data directory
* @returns Promise resolving to the file contents as string
* @throws Error if file cannot be read or doesn't exist
*/
async readFile(path: string): Promise<string> {
const file = await Filesystem.readFile({
path,
directory: Directory.Data,
});
if (file.data instanceof Blob) {
return await file.data.text();
}
return file.data;
}
/**
* Writes content to a file in the app's data directory.
* @param path - Relative path where to write the file
* @param content - Content to write to the file
* @throws Error if write operation fails
*/
async writeFile(path: string, content: string): Promise<void> {
await Filesystem.writeFile({
path,
data: content,
directory: Directory.Data,
});
}
/**
* Deletes a file from the app's data directory.
* @param path - Relative path to the file to delete
* @throws Error if deletion fails or file doesn't exist
*/
async deleteFile(path: string): Promise<void> {
await Filesystem.deleteFile({
path,
directory: Directory.Data,
});
}
/**
* Lists files in the specified directory within app's data directory.
* @param directory - Relative path to the directory to list
* @returns Promise resolving to array of filenames
* @throws Error if directory cannot be read or doesn't exist
*/
async listFiles(directory: string): Promise<string[]> {
const result = await Filesystem.readdir({
path: directory,
directory: Directory.Data,
});
return result.files.map((file) =>
typeof file === "string" ? file : file.name,
);
}
/**
* Opens the device camera to take a picture.
* Configures camera for high quality images with editing enabled.
* @returns Promise resolving to the captured image data
* @throws Error if camera access fails or user cancels
*/
async takePicture(): Promise<ImageResult> {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Base64,
source: CameraSource.Camera,
});
const blob = await this.processImageData(image.base64String);
return {
blob,
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
};
} catch (error) {
logger.error("Error taking picture with Capacitor:", error);
throw new Error("Failed to take picture");
}
}
/**
* Opens the device photo gallery to pick an existing image.
* Configures picker for high quality images with editing enabled.
* @returns Promise resolving to the selected image data
* @throws Error if gallery access fails or user cancels
*/
async pickImage(): Promise<ImageResult> {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Base64,
source: CameraSource.Photos,
});
const blob = await this.processImageData(image.base64String);
return {
blob,
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
};
} catch (error) {
logger.error("Error picking image with Capacitor:", error);
throw new Error("Failed to pick image");
}
}
/**
* Converts base64 image data to a Blob.
* @param base64String - Base64 encoded image data
* @returns Promise resolving to image Blob
* @throws Error if conversion fails
*/
private async processImageData(base64String?: string): Promise<Blob> {
if (!base64String) {
throw new Error("No image data received");
}
// Convert base64 to blob
const byteCharacters = atob(base64String);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: "image/jpeg" });
}
/**
* Checks if running on Capacitor platform.
* @returns true, as this is the Capacitor implementation
*/
isCapacitor(): boolean {
return true;
}
/**
* Checks if running on Electron platform.
* @returns false, as this is not Electron
*/
isElectron(): boolean {
return false;
}
/**
* Checks if running on PyWebView platform.
* @returns false, as this is not PyWebView
*/
isPyWebView(): boolean {
return false;
}
/**
* Checks if running on web platform.
* @returns false, as this is not web
*/
isWeb(): boolean {
return false;
}
/**
* Handles deep link URLs for the application.
* Note: Capacitor handles deep links automatically.
* @param _url - The deep link URL (unused)
*/
async handleDeepLink(_url: string): Promise<void> {
// Capacitor handles deep links automatically
// This is just a placeholder for the interface
return Promise.resolve();
}
getExportInstructions(): string[] {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if (isIOS) {
return [
"On iOS: Choose 'More...' and select a place in iCloud, or go 'Back' and save to another location.",
];
} else {
return [
"On Android: Choose 'Open' and then share to your preferred place.",
];
}
}
getExportSuccessMessage(): string {
return "The backup has been saved to your device.";
}
needsSecondaryDownloadLink(): boolean {
return false;
}
needsDownloadCleanup(): boolean {
return false;
}
async exportDatabase(blob: Blob, fileName: string): Promise<void> {
logger.log("Starting database export on Capacitor platform:", {
fileName,
blobSize: `${blob.size} bytes`,
});
// Create a File object from the Blob
const file = new File([blob], fileName, { type: "application/json" });
try {
logger.log("Attempting to use native share sheet");
// Use the native share sheet
await navigator.share({
files: [file],
title: fileName,
});
logger.log("Database export completed via native share sheet");
} catch (error) {
logger.log("Native share failed, falling back to Capacitor Share API");
// Fallback to Capacitor Share API if Web Share API fails
// First save to temporary file
const base64Data = await this.blobToBase64(blob);
const result = await Filesystem.writeFile({
path: fileName,
data: base64Data,
directory: Directory.Cache, // Use Cache instead of Documents for temporary files
recursive: true,
});
logger.log("Temporary file created for sharing:", result.uri);
// Then share using Capacitor Share API
await Share.share({
title: fileName,
url: result.uri,
});
logger.log("Database export completed via Capacitor Share API");
// Clean up the temporary file
try {
await Filesystem.deleteFile({
path: fileName,
directory: Directory.Cache,
});
logger.log("Temporary file cleaned up successfully");
} catch (cleanupError) {
logger.warn("Failed to clean up temporary file:", cleanupError);
}
}
}
private async blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64data = reader.result as string;
resolve(base64data.split(",")[1]);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
}

View File

@@ -0,0 +1,136 @@
import { ImageResult, PlatformService } from "../PlatformService";
import { logger } from "../../utils/logger";
/**
* Platform service implementation for Electron (desktop) platform.
* Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
*
* @remarks
* This service is intended for desktop application functionality through Electron.
* Future implementations should provide:
* - Native file system access
* - Desktop camera integration
* - System-level features
*/
export class ElectronPlatformService implements PlatformService {
/**
* Reads a file from the filesystem.
* @param _path - Path to the file to read
* @returns Promise that should resolve to file contents
* @throws Error with "Not implemented" message
* @todo Implement file reading using Electron's file system API
*/
async readFile(_path: string): Promise<string> {
throw new Error("Not implemented");
}
/**
* Writes content to a file.
* @param _path - Path where to write the file
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement file writing using Electron's file system API
*/
async writeFile(_path: string, _content: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Deletes a file from the filesystem.
* @param _path - Path to the file to delete
* @throws Error with "Not implemented" message
* @todo Implement file deletion using Electron's file system API
*/
async deleteFile(_path: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Lists files in the specified directory.
* @param _directory - Path to the directory to list
* @returns Promise that should resolve to array of filenames
* @throws Error with "Not implemented" message
* @todo Implement directory listing using Electron's file system API
*/
async listFiles(_directory: string): Promise<string[]> {
throw new Error("Not implemented");
}
/**
* Should open system camera to take a picture.
* @returns Promise that should resolve to captured image data
* @throws Error with "Not implemented" message
* @todo Implement camera access using Electron's media APIs
*/
async takePicture(): Promise<ImageResult> {
logger.error("takePicture not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* Should open system file picker for selecting an image.
* @returns Promise that should resolve to selected image data
* @throws Error with "Not implemented" message
* @todo Implement file picker using Electron's dialog API
*/
async pickImage(): Promise<ImageResult> {
logger.error("pickImage not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* Checks if running on Capacitor platform.
* @returns false, as this is not Capacitor
*/
isCapacitor(): boolean {
return false;
}
/**
* Checks if running on Electron platform.
* @returns true, as this is the Electron implementation
*/
isElectron(): boolean {
return true;
}
/**
* Checks if running on PyWebView platform.
* @returns false, as this is not PyWebView
*/
isPyWebView(): boolean {
return false;
}
/**
* Checks if running on web platform.
* @returns false, as this is not web
*/
isWeb(): boolean {
return false;
}
/**
* Should handle deep link URLs for the desktop application.
* @param _url - The deep link URL to handle
* @throws Error with "Not implemented" message
* @todo Implement deep link handling using Electron's protocol handler
*/
async handleDeepLink(_url: string): Promise<void> {
logger.error("handleDeepLink not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* Exports a database blob to a file.
* @param _blob - The database blob to export
* @param _fileName - The name of the file to save
* @throws Error with "Not implemented" message
* @todo Implement file export using Electron's file system API
*/
async exportDatabase(_blob: Blob, _fileName: string): Promise<void> {
logger.error("exportDatabase not implemented in Electron platform");
throw new Error("Not implemented");
}
}

View File

@@ -0,0 +1,137 @@
import { ImageResult, PlatformService } from "../PlatformService";
import { logger } from "../../utils/logger";
/**
* Platform service implementation for PyWebView platform.
* Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
*
* @remarks
* This service is intended for Python-based desktop applications using pywebview.
* Future implementations should provide:
* - Integration with Python backend file operations
* - System camera access through Python
* - Native system dialogs via pywebview
* - Python-JavaScript bridge functionality
*/
export class PyWebViewPlatformService implements PlatformService {
/**
* Reads a file using the Python backend.
* @param _path - Path to the file to read
* @returns Promise that should resolve to file contents
* @throws Error with "Not implemented" message
* @todo Implement file reading through pywebview's Python-JavaScript bridge
*/
async readFile(_path: string): Promise<string> {
throw new Error("Not implemented");
}
/**
* Writes content to a file using the Python backend.
* @param _path - Path where to write the file
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement file writing through pywebview's Python-JavaScript bridge
*/
async writeFile(_path: string, _content: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Deletes a file using the Python backend.
* @param _path - Path to the file to delete
* @throws Error with "Not implemented" message
* @todo Implement file deletion through pywebview's Python-JavaScript bridge
*/
async deleteFile(_path: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Lists files in the specified directory using the Python backend.
* @param _directory - Path to the directory to list
* @returns Promise that should resolve to array of filenames
* @throws Error with "Not implemented" message
* @todo Implement directory listing through pywebview's Python-JavaScript bridge
*/
async listFiles(_directory: string): Promise<string[]> {
throw new Error("Not implemented");
}
/**
* Should open system camera through Python backend.
* @returns Promise that should resolve to captured image data
* @throws Error with "Not implemented" message
* @todo Implement camera access using Python's camera libraries
*/
async takePicture(): Promise<ImageResult> {
logger.error("takePicture not implemented in PyWebView platform");
throw new Error("Not implemented");
}
/**
* Should open system file picker through pywebview.
* @returns Promise that should resolve to selected image data
* @throws Error with "Not implemented" message
* @todo Implement file picker using pywebview's file dialog API
*/
async pickImage(): Promise<ImageResult> {
logger.error("pickImage not implemented in PyWebView platform");
throw new Error("Not implemented");
}
/**
* Checks if running on Capacitor platform.
* @returns false, as this is not Capacitor
*/
isCapacitor(): boolean {
return false;
}
/**
* Checks if running on Electron platform.
* @returns false, as this is not Electron
*/
isElectron(): boolean {
return false;
}
/**
* Checks if running on PyWebView platform.
* @returns true, as this is the PyWebView implementation
*/
isPyWebView(): boolean {
return true;
}
/**
* Checks if running on web platform.
* @returns false, as this is not web
*/
isWeb(): boolean {
return false;
}
/**
* Should handle deep link URLs through the Python backend.
* @param _url - The deep link URL to handle
* @throws Error with "Not implemented" message
* @todo Implement deep link handling using Python's URL handling capabilities
*/
async handleDeepLink(_url: string): Promise<void> {
logger.error("handleDeepLink not implemented in PyWebView platform");
throw new Error("Not implemented");
}
/**
* Exports a database blob to a file using the Python backend.
* @param _blob - The database blob to export
* @param _fileName - The name of the file to save
* @throws Error with "Not implemented" message
* @todo Implement file export through pywebview's Python-JavaScript bridge
*/
async exportDatabase(_blob: Blob, _fileName: string): Promise<void> {
logger.error("exportDatabase not implemented in PyWebView platform");
throw new Error("Not implemented");
}
}

View File

@@ -0,0 +1,261 @@
import { ImageResult, PlatformService } from "../PlatformService";
import { logger } from "../../utils/logger";
/**
* Platform service implementation for web browser platform.
* Implements the PlatformService interface with web-specific functionality.
*
* @remarks
* This service provides web-based implementations for:
* - Image capture using the browser's file input
* - Image selection from local filesystem
* - Image processing and conversion
*
* Note: File system operations are not available in the web platform
* due to browser security restrictions. These methods throw appropriate errors.
*/
export class WebPlatformService implements PlatformService {
/**
* Not supported in web platform.
* @param _path - Unused path parameter
* @throws Error indicating file system access is not available
*/
async readFile(_path: string): Promise<string> {
throw new Error("File system access not available in web platform");
}
/**
* Not supported in web platform.
* @param _path - Unused path parameter
* @param _content - Unused content parameter
* @throws Error indicating file system access is not available
*/
async writeFile(_path: string, _content: string): Promise<void> {
throw new Error("File system access not available in web platform");
}
/**
* Not supported in web platform.
* @param _path - Unused path parameter
* @throws Error indicating file system access is not available
*/
async deleteFile(_path: string): Promise<void> {
throw new Error("File system access not available in web platform");
}
/**
* Not supported in web platform.
* @param _directory - Unused directory parameter
* @throws Error indicating file system access is not available
*/
async listFiles(_directory: string): Promise<string[]> {
throw new Error("File system access not available in web platform");
}
/**
* Opens a file input dialog configured for camera capture.
* Creates a temporary file input element to access the device camera.
*
* @returns Promise resolving to the captured image data
* @throws Error if image capture fails or no image is selected
*
* @remarks
* Uses the 'capture' attribute to prefer the device camera.
* Falls back to file selection if camera is not available.
* Processes the captured image to ensure consistent format.
*/
async takePicture(): Promise<ImageResult> {
return new Promise((resolve, reject) => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.capture = "environment";
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
try {
const blob = await this.processImageFile(file);
resolve({
blob,
fileName: file.name || "photo.jpg",
});
} catch (error) {
logger.error("Error processing camera image:", error);
reject(new Error("Failed to process camera image"));
}
} else {
reject(new Error("No image captured"));
}
};
input.click();
});
}
/**
* Opens a file input dialog for selecting an image file.
* Creates a temporary file input element to access local files.
*
* @returns Promise resolving to the selected image data
* @throws Error if image processing fails or no image is selected
*
* @remarks
* Allows selection of any image file type.
* Processes the selected image to ensure consistent format.
*/
async pickImage(): Promise<ImageResult> {
return new Promise((resolve, reject) => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
try {
const blob = await this.processImageFile(file);
resolve({
blob,
fileName: file.name || "photo.jpg",
});
} catch (error) {
logger.error("Error processing picked image:", error);
reject(new Error("Failed to process picked image"));
}
} else {
reject(new Error("No image selected"));
}
};
input.click();
});
}
/**
* Processes an image file to ensure consistent format.
* Converts the file to a data URL and then to a Blob.
*
* @param file - The image File object to process
* @returns Promise resolving to processed image Blob
* @throws Error if file reading or conversion fails
*
* @remarks
* This method ensures consistent image format across different
* input sources by converting through data URL to Blob.
*/
private async processImageFile(file: File): Promise<Blob> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target?.result as string;
// Convert to blob to ensure consistent format
fetch(dataUrl)
.then((res) => res.blob())
.then((blob) => resolve(blob))
.catch((error) => {
logger.error("Error converting data URL to blob:", error);
reject(error);
});
};
reader.onerror = (error) => {
logger.error("Error reading file:", error);
reject(error);
};
reader.readAsDataURL(file);
});
}
/**
* Checks if running on Capacitor platform.
* @returns false, as this is not Capacitor
*/
isCapacitor(): boolean {
return false;
}
/**
* Checks if running on Electron platform.
* @returns false, as this is not Electron
*/
isElectron(): boolean {
return false;
}
/**
* Checks if running on PyWebView platform.
* @returns false, as this is not PyWebView
*/
isPyWebView(): boolean {
return false;
}
/**
* Checks if running on web platform.
* @returns true, as this is the web implementation
*/
isWeb(): boolean {
return true;
}
/**
* Handles deep link URLs in the web platform.
* Deep links are handled through URL parameters in the web environment.
*
* @param _url - The deep link URL to handle (unused in web implementation)
* @returns Promise that resolves immediately as web handles URLs naturally
*/
async handleDeepLink(_url: string): Promise<void> {
// Web platform can handle deep links through URL parameters
return Promise.resolve();
}
getExportInstructions(): string[] {
return [
"After the download, you can save the file in your preferred storage location.",
];
}
getExportSuccessMessage(): string {
return "See your downloads directory for the backup. It is in the Dexie format.";
}
needsSecondaryDownloadLink(): boolean {
return true;
}
needsDownloadCleanup(): boolean {
return true;
}
async exportDatabase(blob: Blob, fileName: string): Promise<void> {
logger.log("Starting database export on web platform:", {
fileName,
blobSize: `${blob.size} bytes`,
});
try {
logger.log("Creating download link for database export");
// Create a download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
logger.log("Triggering download");
// Trigger the download
document.body.appendChild(a);
a.click();
logger.log("Cleaning up download link");
// Clean up
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
logger.log("Database export completed successfully");
} catch (error) {
logger.error("Failed to export database:", error);
throw error;
}
}
}

View File

@@ -420,53 +420,7 @@
</button>
</div>
<div
id="sectionDataExport"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
>
<div class="mb-2 font-bold">Data Export</div>
<router-link
v-if="activeDid"
:to="{ name: 'seed-backup' }"
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 mb-2 mt-2"
>
Backup Identifier Seed
</router-link>
<button
:class="computedStartDownloadLinkClassNames()"
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()"
>
Download Settings & Contacts
<br />
(excluding Identifier Data)
</button>
<a
ref="downloadLink"
:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
</a>
<div class="mt-4">
<p>
After the download, you can save the file in your preferred storage
location.
</p>
<ul>
<li class="list-disc list-outside ml-4">
On iOS: Choose "More..." and select a place in iCloud, or go "Back"
and save to another location.
</li>
<li class="list-disc list-outside ml-4">
On Android: Choose "Open" and then share
<font-awesome icon="share-nodes" class="fa-fw" />
to your prefered place.
</li>
</ul>
</div>
</div>
<DataExportSection :active-did="activeDid" />
<!-- id used by puppeteer test script -->
<h3
@@ -946,6 +900,7 @@ import PushNotificationPermission from "../components/PushNotificationPermission
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import DataExportSection from "../components/DataExportSection.vue";
import {
AppString,
DEFAULT_IMAGE_API_SERVER,
@@ -999,6 +954,7 @@ const inputImportFileNameRef = ref<Blob>();
QuickNav,
TopMessage,
UserNameDialog,
DataExportSection,
},
})
export default class AccountViewView extends Vue {

View File

@@ -137,7 +137,7 @@ export default class SharedPhotoView extends Vue {
// this might be wrong since "name" goes with params, but it works so test well when you change it
query: {
destinationPathAfter: "/",
hideBackButton: true,
hideBackButton: "true",
imageUrl: url,
recipientDid: this.activeDid,
},
@@ -221,13 +221,63 @@ export default class SharedPhotoView extends Vue {
this.uploading = false;
} catch (error) {
logger.error("Error uploading the image", error);
// Log the raw error first
logger.error("Raw error object:", JSON.stringify(error, null, 2));
let errorMessage = "There was an error saving the picture.";
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const statusText = error.response?.statusText;
const data = error.response?.data;
// Log detailed error information
logger.error("Upload error details:", {
status,
statusText,
data: JSON.stringify(data, null, 2),
message: error.message,
config: {
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers,
},
});
if (status === 401) {
errorMessage = "Authentication failed. Please try logging in again.";
} else if (status === 413) {
errorMessage = "Image file is too large. Please try a smaller image.";
} else if (status === 415) {
errorMessage =
"Unsupported image format. Please try a different image.";
} else if (status && status >= 500) {
errorMessage = "Server error. Please try again later.";
} else if (data?.message) {
errorMessage = data.message;
}
} else if (error instanceof Error) {
// Log non-Axios error with full details
logger.error("Non-Axios error details:", {
name: error.name,
message: error.message,
stack: error.stack,
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
});
} else {
// Log any other type of error
logger.error("Unknown error type:", {
error: JSON.stringify(error, null, 2),
type: typeof error,
});
}
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error saving the picture.",
text: errorMessage,
},
5000,
);

View File

@@ -1,35 +1,31 @@
{
"compilerOptions": {
"target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers
"module": "ESNext", // Use ES modules
"strict": true, // Enable all strict type checking options
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler
"moduleResolution": "node", // Use Node.js style module resolution
"experimentalDecorators": true,
"esModuleInterop": true, // Enables compatibility with CommonJS modules for default imports
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": "./src", // Base directory to resolve non-relative module names
"module": "ESNext", // Use ES modules
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler
"strict": true, // Enable all strict type checking options
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"experimentalDecorators": true,
"paths": {
"@/components/*": ["components/*"],
"@/views/*": ["views/*"],
"@/db/*": ["db/*"],
"@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"],
"@/store/*": ["store/*"]
},
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
"@/*": ["src/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"test-playwright/**/*.ts",
"test-playwright/**/*.tsx"
"src/**/*.vue"
],
"exclude": [
"node_modules"
]
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.*"]
}

View File

@@ -36,7 +36,21 @@ export async function createBuildConfig(mode: string) {
assetsDir: 'assets',
chunkSizeWarningLimit: 1000,
rollupOptions: {
external: isCapacitor ? ['@capacitor/app'] : []
external: isCapacitor
? [
'@capacitor/app',
'@capacitor/share',
'@capacitor/filesystem',
'@capacitor/camera',
'@capacitor/core'
]
: [
'@capacitor/app',
'@capacitor/share',
'@capacitor/filesystem',
'@capacitor/camera',
'@capacitor/core'
]
}
},
define: {