Compare commits
37 Commits
db-backup-
...
build-ios
| Author | SHA1 | Date | |
|---|---|---|---|
| ca455e9593 | |||
| 5ada70b05e | |||
|
|
4f9b146a66 | ||
|
|
2b638ce2a7 | ||
|
|
0b528af2a6 | ||
|
|
008211bc21 | ||
|
|
6955a36458 | ||
|
|
ba079ea983 | ||
|
|
d7b3c5ec9d | ||
|
|
d83a25f47e | ||
|
|
fb40dc0ff7 | ||
|
|
d03fa55001 | ||
|
|
c8eff4d39e | ||
|
|
b8a7771edf | ||
|
|
5d845fb112 | ||
|
|
660f2170de | ||
|
|
94bd649003 | ||
|
|
b2d628cfeb | ||
|
|
00e52f8dca | ||
|
|
073ce24f43 | ||
|
|
2c84bb50b3 | ||
|
|
abf18835f6 | ||
|
|
f72562804d | ||
|
|
bdc5ffafc1 | ||
| 634395ff38 | |||
| da1f08ebaa | |||
| 4ee3ce0061 | |||
| 654c67af72 | |||
| b244f609b3 | |||
| 9c84302c2e | |||
| ca37c30180 | |||
| 130139e2af | |||
| 9802deb17c | |||
| 76c983ea3e | |||
| 114ef440b8 | |||
| b58d510f24 | |||
|
|
770c0fa77c |
@@ -1 +0,0 @@
|
||||
PLATFORM=electron
|
||||
@@ -1,5 +0,0 @@
|
||||
PLATFORM=capacitor
|
||||
VITE_ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
|
||||
VITE_PARTNER_API_URL=https://test-api.partner.ch/api/v2
|
||||
VITE_IMAGE_API_URL=https://test-api.images.ch/api/v2
|
||||
VITE_PUSH_SERVER_URL=https://test-api.push.ch/api/v2
|
||||
@@ -27,9 +27,6 @@ module.exports = {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}]
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||
},
|
||||
};
|
||||
|
||||
17
.gitignore
vendored
@@ -39,28 +39,19 @@ pnpm-debug.log*
|
||||
/test-playwright-results/
|
||||
playwright-tests
|
||||
dist-electron-packages
|
||||
ios
|
||||
.ruby-version
|
||||
+.env
|
||||
|
||||
# Generated test files
|
||||
.generated/
|
||||
|
||||
# Fastlane
|
||||
ios/fastlane/report.xml
|
||||
ios/fastlane/Preview.html
|
||||
ios/fastlane/screenshots
|
||||
ios/fastlane/test_output
|
||||
android/fastlane/report.xml
|
||||
android/fastlane/Preview.html
|
||||
android/fastlane/screenshots
|
||||
android/fastlane/test_output
|
||||
.env.default
|
||||
vendor/
|
||||
|
||||
# Build logs
|
||||
build_logs/
|
||||
|
||||
# Android generated assets
|
||||
android/app/src/main/assets/public/assets/
|
||||
|
||||
android/app/src/main/assets/public
|
||||
android/app/src/main/res
|
||||
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
|
||||
android/.gradle/file-system.probe
|
||||
109
BUILDING.md
@@ -9,8 +9,19 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
||||
- Node.js (LTS version recommended)
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
- For iOS builds: macOS with Xcode installed
|
||||
- For Android builds: Android Studio with SDK installed
|
||||
- For iOS builds: macOS with Xcode and ruby gems & bundle
|
||||
- pkgx +rubygems.org sh
|
||||
|
||||
- ... and you may have to fix these, especially with pkgx
|
||||
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
|
||||
- For desktop builds: Additional build tools based on your OS
|
||||
|
||||
## Forks
|
||||
@@ -22,26 +33,23 @@ If you have forked this to make your own app, you'll want to customize the iOS &
|
||||
npx cap add ios
|
||||
```
|
||||
|
||||
You'll also want to edit the deep link configuration.
|
||||
You'll also want to edit the deep link configuration (see below).
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone [repository-url]
|
||||
cd TimeSafari
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Web Build
|
||||
## Web Dev Locally
|
||||
|
||||
To build for web deployment:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Web Build for Server
|
||||
|
||||
1. Run the production build:
|
||||
|
||||
@@ -49,17 +57,66 @@ To build for web deployment:
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. The built files will be in the `dist` directory.
|
||||
The built files will be in the `dist` directory.
|
||||
|
||||
3. To test the production build locally:
|
||||
2. To test the production build locally:
|
||||
|
||||
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compile and minify for test & production
|
||||
|
||||
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
|
||||
|
||||
* `npx prettier --write ./sw_scripts/`
|
||||
|
||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||
|
||||
* Commit everything (since the commit hash is used the app).
|
||||
|
||||
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
||||
|
||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
|
||||
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```bash
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||
```
|
||||
|
||||
... and transfer to the test server:
|
||||
|
||||
```bash
|
||||
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
|
||||
```
|
||||
|
||||
(Let's replace that with a .env.development or .env.staging file.)
|
||||
|
||||
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
|
||||
|
||||
* For prod, get on the server and run the correct build:
|
||||
|
||||
... and log onto the server:
|
||||
|
||||
* `pkgx +npm sh`
|
||||
|
||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
|
||||
|
||||
(The plain `npm run build` uses the .env.production file.)
|
||||
|
||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||
|
||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||
|
||||
|
||||
|
||||
|
||||
## Desktop Build (Electron)
|
||||
|
||||
### Building for Linux
|
||||
### Linux Build
|
||||
|
||||
1. Build the electron app in production mode:
|
||||
|
||||
@@ -142,6 +199,12 @@ Prerequisites: macOS with Xcode installed
|
||||
|
||||
4. Use Xcode to build and run on simulator or device.
|
||||
|
||||
#### First-time iOS Configuration
|
||||
|
||||
- Generate certificates inside XCode.
|
||||
|
||||
- Right-click on App and under Signing & Capabilities set the Team.
|
||||
|
||||
### Android Build
|
||||
|
||||
Prerequisites: Android Studio with SDK installed
|
||||
@@ -152,6 +215,9 @@ Prerequisites: Android Studio with SDK installed
|
||||
rm -rf dist
|
||||
npm run build:web
|
||||
npm run build:capacitor
|
||||
cd android
|
||||
./gradlew clean
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
2. Update Android project with latest build:
|
||||
@@ -174,7 +240,7 @@ Prerequisites: Android Studio with SDK installed
|
||||
|
||||
5. Use Android Studio to build and run on emulator or device.
|
||||
|
||||
## Building Android from the console
|
||||
## Android Build from the console
|
||||
|
||||
```bash
|
||||
cd android
|
||||
@@ -187,11 +253,18 @@ Prerequisites: Android Studio with SDK installed
|
||||
... or, to create the `aab` file, `bundle` instead of `build`:
|
||||
|
||||
```bash
|
||||
./gradlew bundle -Dlint.baselines.continue=true
|
||||
./gradlew bundleDebug -Dlint.baselines.continue=true
|
||||
```
|
||||
|
||||
... or, to create a signed release, add the app/gradle.properties.secrets file (see properties at top of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file, then `bundleRelease`:
|
||||
|
||||
```bash
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
```
|
||||
|
||||
|
||||
## Configuring Android for deep links
|
||||
|
||||
## First-time Android Configuration for deep links
|
||||
|
||||
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
||||
|
||||
|
||||
53
README.md
@@ -19,59 +19,6 @@ npm run dev
|
||||
|
||||
See [BUILDING.md](BUILDING.md) for more details.
|
||||
|
||||
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
|
||||
|
||||
|
||||
### Run all UI tests
|
||||
|
||||
Look at [BUILDING.md](BUILDING.md) for the "test-all" instructions and [TESTING.md](test-playwright/TESTING.md) for more details.
|
||||
|
||||
|
||||
### Compile and minify for test & production
|
||||
|
||||
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
|
||||
|
||||
* `npx prettier --write ./sw_scripts/`
|
||||
|
||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||
|
||||
* Commit everything (since the commit hash is used the app).
|
||||
|
||||
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
||||
|
||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
|
||||
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```bash
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||
```
|
||||
|
||||
... and transfer to the test server:
|
||||
|
||||
```bash
|
||||
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
|
||||
```
|
||||
|
||||
(Let's replace that with a .env.development or .env.staging file.)
|
||||
|
||||
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
|
||||
|
||||
* For prod, get on the server and run the correct build:
|
||||
|
||||
... and log onto the server:
|
||||
|
||||
* `pkgx +npm sh`
|
||||
|
||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
|
||||
|
||||
(The plain `npm run build` uses the .env.production file.)
|
||||
|
||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||
|
||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3
android/.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
app/gradle.properties.secrets
|
||||
app/time-safari-upload-key-pkcs12.jks
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#Thu Apr 03 10:21:42 UTC 2025
|
||||
#Wed Apr 09 09:01:13 UTC 2025
|
||||
gradle.version=8.11.1
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
// These are sample values to set in gradle.properties.secrets
|
||||
// MY_KEYSTORE_FILE=time-safari-upload-key-pkcs12.jks
|
||||
// MY_KEYSTORE_PASSWORD=...
|
||||
// MY_KEY_ALIAS=time-safari-key-alias
|
||||
// MY_KEY_PASSWORD=...
|
||||
|
||||
// Try to load from environment variables first
|
||||
project.ext.MY_KEYSTORE_FILE = System.getenv('ANDROID_KEYSTORE_FILE') ?: ""
|
||||
project.ext.MY_KEYSTORE_PASSWORD = System.getenv('ANDROID_KEYSTORE_PASSWORD') ?: ""
|
||||
project.ext.MY_KEY_ALIAS = System.getenv('ANDROID_KEY_ALIAS') ?: ""
|
||||
project.ext.MY_KEY_PASSWORD = System.getenv('ANDROID_KEY_PASSWORD') ?: ""
|
||||
|
||||
// If no environment variables, try to load from secrets file
|
||||
if (!project.ext.MY_KEYSTORE_FILE) {
|
||||
def secretsPropertiesFile = rootProject.file("app/gradle.properties.secrets")
|
||||
if (secretsPropertiesFile.exists()) {
|
||||
Properties secretsProperties = new Properties()
|
||||
secretsProperties.load(new FileInputStream(secretsPropertiesFile))
|
||||
secretsProperties.each { name, value ->
|
||||
project.ext[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'app.timesafari'
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "app.timesafari"
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 10
|
||||
versionName "0.4.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -16,10 +40,41 @@ android {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
if (project.ext.MY_KEYSTORE_FILE &&
|
||||
project.ext.MY_KEYSTORE_PASSWORD &&
|
||||
project.ext.MY_KEY_ALIAS &&
|
||||
project.ext.MY_KEY_PASSWORD) {
|
||||
|
||||
storeFile file(project.ext.MY_KEYSTORE_FILE)
|
||||
storePassword project.ext.MY_KEYSTORE_PASSWORD
|
||||
keyAlias project.ext.MY_KEY_ALIAS
|
||||
keyPassword project.ext.MY_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
// Only sign if we have the signing config
|
||||
if (signingConfigs.release.storeFile != null) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = true
|
||||
}
|
||||
density {
|
||||
enableSplit = true
|
||||
}
|
||||
abi {
|
||||
enableSplit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ android {
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capawesome-capacitor-file-picker')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("app.timesafari", appContext.getPackageName());
|
||||
assertEquals("app.timesafari.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -8,7 +7,6 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
@@ -16,7 +14,6 @@
|
||||
android:label="@string/title_activity_main"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -28,7 +25,6 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="timesafari" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
@@ -36,13 +32,13 @@
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
</manifest>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"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"
|
||||
@@ -10,5 +14,9 @@
|
||||
{
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capawesome/capacitor-file-picker",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>TimeSafari</title>
|
||||
<script type="module" crossorigin src="/assets/index-KPivi3wg.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 23 KiB |
@@ -2,4 +2,5 @@
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
<files-path name="my_files" path="." />
|
||||
</paths>
|
||||
@@ -5,8 +5,14 @@ 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')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
include ':capawesome-capacitor-file-picker'
|
||||
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
|
||||
|
||||
BIN
assets/icon-only.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 190 KiB |
16
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
App/build
|
||||
App/output
|
||||
App/Pods
|
||||
|
||||
App/*.xcodeproj/xcuserdata/
|
||||
App/*.xcworkspace/xcuserdata/
|
||||
App/*/public
|
||||
|
||||
# Generated Config files
|
||||
App/*/capacitor.config.json
|
||||
App/*/config.xml
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
DerivedData
|
||||
412
ios/App/App.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,412 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 48;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2BC611FE3D7967BDB623FF21 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0C2082015AEE6A0776A3EAB /* Pods_App.framework */; };
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
821226CEE4D47A540167CC8F /* Pods-Time Safari.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Time Safari.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Time Safari/Pods-Time Safari.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E0C2082015AEE6A0776A3EAB /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EF03C3F99471948925ED5AC3 /* Pods-Time Safari.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Time Safari.release.xcconfig"; path = "Pods/Target Support Files/Pods-Time Safari/Pods-Time Safari.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2BC611FE3D7967BDB623FF21 /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E0C2082015AEE6A0776A3EAB /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
821226CEE4D47A540167CC8F /* Pods-Time Safari.debug.xcconfig */,
|
||||
EF03C3F99471948925ED5AC3 /* Pods-Time Safari.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
productName = "Time Safari";
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastUpgradeCheck = 920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.4.4;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.4.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
10
ios/App/App.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:App.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
49
ios/App/App/AppDelegate.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 116 KiB |
14
ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
6
ios/App/App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
32
ios/App/App/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
19
ios/App/App/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
64
ios/App/App/Info.plist
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>TimeSafari</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Upload photos and scan friends' QR codes</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Upload photos for gifts</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>app.timesafari</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array></dict>
|
||||
</plist>
|
||||
28
ios/App/Podfile
Normal file
@@ -0,0 +1,28 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '13.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||
# Requires CocoaPods 1.6 or newer
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
end
|
||||
52
ios/App/Podfile.lock
Normal file
@@ -0,0 +1,52 @@
|
||||
PODS:
|
||||
- Capacitor (6.2.1):
|
||||
- CapacitorCordova
|
||||
- CapacitorApp (6.0.2):
|
||||
- Capacitor
|
||||
- CapacitorCamera (6.1.2):
|
||||
- Capacitor
|
||||
- CapacitorCordova (6.2.1)
|
||||
- CapacitorFilesystem (6.0.3):
|
||||
- Capacitor
|
||||
- CapacitorShare (6.0.3):
|
||||
- Capacitor
|
||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
||||
- Capacitor
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorApp:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/@capacitor/camera"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorFilesystem:
|
||||
:path: "../../node_modules/@capacitor/filesystem"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/@capacitor/share"
|
||||
CapawesomeCapacitorFilePicker:
|
||||
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
|
||||
PODFILE CHECKSUM: 1e9280368fd410520414f5741bf8fdfe7847b965
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
2112
package-lock.json
generated
27
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "0.4.4",
|
||||
"description": "TimeSafari Desktop Application",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "TimeSafari Team"
|
||||
"name": "Time Safari Team"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --config vite.config.dev.mts",
|
||||
@@ -27,6 +27,7 @@
|
||||
"build:web": "vite build --config vite.config.web.mts",
|
||||
"electron:dev": "npm run build && electron dist-electron",
|
||||
"electron:start": "electron dist-electron",
|
||||
"build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||
@@ -44,14 +45,15 @@
|
||||
"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.1",
|
||||
"@capacitor/filesystem": "^6.0.3",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
"@electron/remote": "^2.1.2",
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
"@ethersproject/wallet": "^5.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
@@ -101,13 +103,13 @@
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"qr-code-generator-vue3": "^1.4.21",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"ramda": "^0.29.1",
|
||||
"readable-stream": "^4.5.2",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"simple-vue-camera": "^1.1.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"three": "^0.156.1",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"vue": "^3.5.13",
|
||||
@@ -126,7 +128,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/ramda": "^0.29.11",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
@@ -136,12 +138,8 @@
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"assert": "^2.1.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browserify-fs": "^1.0.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -149,21 +147,14 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"markdownlint": "^0.37.4",
|
||||
"markdownlint-cli": "^0.44.0",
|
||||
"npm-check-updates": "^17.1.13",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tty-browserify": "^0.0.1",
|
||||
"typescript": "~5.2.2",
|
||||
"url": "^0.11.4",
|
||||
"util": "^0.12.5",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-pwa": "^0.19.8"
|
||||
},
|
||||
|
||||
@@ -69,11 +69,11 @@ export default defineConfig({
|
||||
permissions: ["clipboard-read"],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
|
||||
@@ -40,10 +40,10 @@ export default defineConfig({
|
||||
permissions: ["clipboard-read"],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
|
||||
22
scripts/copy-web-assets.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Clean the public directory
|
||||
rm -rf android/app/src/main/assets/public/*
|
||||
|
||||
# Copy web assets
|
||||
cp -r dist/* android/app/src/main/assets/public/
|
||||
|
||||
# Ensure the directory structure exists
|
||||
mkdir -p android/app/src/main/assets/public/assets
|
||||
|
||||
# Copy the main index file
|
||||
cp dist/index.html android/app/src/main/assets/public/
|
||||
|
||||
# Copy all assets
|
||||
cp -r dist/assets/* android/app/src/main/assets/public/assets/
|
||||
|
||||
# Copy other necessary files
|
||||
cp dist/favicon.ico android/app/src/main/assets/public/
|
||||
cp dist/robots.txt android/app/src/main/assets/public/
|
||||
|
||||
echo "Web assets copied successfully!"
|
||||
22
scripts/generate-icons.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create directories if they don't exist
|
||||
mkdir -p android/app/src/main/res/mipmap-mdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-hdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-xhdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-xxhdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-xxxhdpi
|
||||
|
||||
# Generate placeholder icons using ImageMagick
|
||||
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png
|
||||
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png
|
||||
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
|
||||
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
|
||||
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
|
||||
|
||||
# Copy to round versions
|
||||
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
|
||||
@@ -103,7 +103,7 @@ const cleanIosPlatform = async (log) => {
|
||||
// Get app name from package.json
|
||||
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
|
||||
const appName = packageJson.name || 'App';
|
||||
const appId = packageJson.capacitor?.appId || 'io.ionic.starter';
|
||||
const appId = packageJson.build.appId || 'io.ionic.starter';
|
||||
|
||||
// Create a minimal capacitor config
|
||||
const capacitorConfig = `
|
||||
@@ -467,12 +467,12 @@ const configureIosProject = async (log) => {
|
||||
// Build and test iOS project
|
||||
const buildAndTestIos = async (log, simulator) => {
|
||||
const simulatorName = simulator[0].name;
|
||||
log('🏗️ Building iOS project...');
|
||||
log('🏗️ Building iOS project...', simulator[0]);
|
||||
execSync('cd ios/App && xcodebuild clean -workspace App.xcworkspace -scheme App', { stdio: 'inherit' });
|
||||
log('✅ Xcode clean completed');
|
||||
|
||||
log(`🏗️ Building for simulator: ${simulatorName}`);
|
||||
execSync(`cd ios/App && xcodebuild build -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,name=${simulatorName}"`, { stdio: 'inherit' });
|
||||
execSync(`cd ios/App && xcodebuild build -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,OS=17.2,name=${simulatorName}"`, { stdio: 'inherit' });
|
||||
log('✅ Xcode build completed');
|
||||
|
||||
// Check if the project is configured for testing by querying the scheme capabilities
|
||||
|
||||
@@ -62,10 +62,12 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="relative flex justify-between gap-4 max-w-lg mx-auto mb-5">
|
||||
<div
|
||||
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5"
|
||||
>
|
||||
<!-- Source -->
|
||||
<div
|
||||
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||
>
|
||||
<div class="relative w-fit mx-auto">
|
||||
<div>
|
||||
@@ -96,7 +98,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="record.providerPlanName || record.giver.known"
|
||||
class="text-xs mt-2 line-clamp-3 sm:line-clamp-2"
|
||||
class="text-xs mt-2 truncate"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="record.providerPlanName ? 'users' : 'user'"
|
||||
@@ -108,9 +110,9 @@
|
||||
|
||||
<!-- Arrow -->
|
||||
<div
|
||||
class="absolute inset-x-28 sm:inset-x-40 mx-2 top-1/2 -translate-y-1/2"
|
||||
class="absolute inset-x-[8rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<div class="text-sm text-center leading-none font-semibold">
|
||||
<div class="text-sm text-center leading-none font-semibold pe-[15px]">
|
||||
{{ fetchAmount }}
|
||||
</div>
|
||||
|
||||
@@ -127,7 +129,7 @@
|
||||
|
||||
<!-- Destination -->
|
||||
<div
|
||||
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||
>
|
||||
<div class="relative w-fit mx-auto">
|
||||
<div>
|
||||
@@ -158,7 +160,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="record.recipientProjectName || record.receiver.known"
|
||||
class="text-xs mt-2 line-clamp-3 sm:line-clamp-2"
|
||||
class="text-xs mt-2 truncate"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="record.recipientProjectName ? 'users' : 'user'"
|
||||
@@ -192,7 +194,6 @@ import ProjectIcon from "./ProjectIcon.vue";
|
||||
EntityIcon,
|
||||
ProjectIcon,
|
||||
},
|
||||
emits: ["loadClaim", "viewImage", "cacheImage", "confirmClaim"],
|
||||
})
|
||||
export default class ActivityListItem extends Vue {
|
||||
@Prop() record!: GiveRecordWithContactInfo;
|
||||
|
||||
196
src/components/DataExportSection.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
/** * 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
|
||||
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="platformCapabilities.needsFileHandlingInstructions" class="mt-4">
|
||||
<p>
|
||||
After the download, you can save the file in your preferred storage
|
||||
location.
|
||||
</p>
|
||||
<ul>
|
||||
<li
|
||||
v-if="platformCapabilities.isIOS"
|
||||
class="list-disc list-outside ml-4"
|
||||
>
|
||||
On iOS: You will be prompted to choose a location to save your backup
|
||||
file.
|
||||
</li>
|
||||
<li
|
||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
||||
class="list-disc list-outside ml-4"
|
||||
>
|
||||
On Android: You will be prompted to choose a location to save your
|
||||
backup file.
|
||||
</li>
|
||||
</ul>
|
||||
</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,
|
||||
PlatformCapabilities,
|
||||
} 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 =
|
||||
PlatformServiceFactory.getInstance();
|
||||
|
||||
/**
|
||||
* Platform capabilities for the current platform
|
||||
*/
|
||||
private get platformCapabilities(): PlatformCapabilities {
|
||||
return this.platformService.getCapabilities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook to clean up resources
|
||||
* Revokes object URL when component is unmounted (web platform only)
|
||||
*/
|
||||
beforeUnmount() {
|
||||
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) {
|
||||
URL.revokeObjectURL(this.downloadUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const blob = await db.export({ prettyJson: true });
|
||||
const fileName = `${db.name}-backup.json`;
|
||||
|
||||
if (this.platformCapabilities.hasFileDownload) {
|
||||
// Web platform: Use download link
|
||||
this.downloadUrl = URL.createObjectURL(blob);
|
||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||
downloadAnchor.href = this.downloadUrl;
|
||||
downloadAnchor.download = fileName;
|
||||
downloadAnchor.click();
|
||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||
} else if (this.platformCapabilities.hasFileSystem) {
|
||||
// Native platform: Write to app directory
|
||||
const content = await blob.text();
|
||||
await this.platformService.writeAndShareFile(fileName, content);
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Export Successful",
|
||||
text: this.platformCapabilities.hasFileDownload
|
||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
||||
: "Please choose a location to save your backup file.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Export Error:", error);
|
||||
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.platformCapabilities.hasFileDownload,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.platformCapabilities.hasFileDownload,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,101 +1,42 @@
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<template>
|
||||
<div class="w-fit">
|
||||
<img
|
||||
v-if="hasImage"
|
||||
:src="imageUrl"
|
||||
class="rounded cursor-pointer"
|
||||
:width="iconSize"
|
||||
:height="iconSize"
|
||||
@click="handleClick"
|
||||
/>
|
||||
<div v-else class="cursor-pointer" @click="handleClick">
|
||||
<img
|
||||
v-if="!identifier"
|
||||
:src="blankSquareUrl"
|
||||
class="rounded"
|
||||
:width="iconSize"
|
||||
:height="iconSize"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
:width="iconSize"
|
||||
:height="iconSize"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g v-for="(path, index) in avatarPaths" :key="index">
|
||||
<path :d="path" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="w-fit" v-html="generateIcon()"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||
import { avataaars } from "@dicebear/collection";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop({ required: false }) contact?: Contact;
|
||||
@Prop contact: Contact;
|
||||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
||||
@Prop iconSize = 0;
|
||||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
||||
|
||||
private avatarPaths: string[] = [];
|
||||
private blankSquareUrl =
|
||||
import.meta.env.VITE_BASE_URL + "assets/blank-square.svg";
|
||||
|
||||
get imageUrl(): string {
|
||||
return this.contact?.profileImageUrl || this.profileImageUrl;
|
||||
}
|
||||
|
||||
get hasImage(): boolean {
|
||||
return !!this.imageUrl;
|
||||
}
|
||||
|
||||
get identifier(): string | undefined {
|
||||
return this.contact?.did || this.entityId;
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
try {
|
||||
// Emit a simple event without passing the event object
|
||||
this.$emit("click");
|
||||
} catch (error) {
|
||||
logger.error("Error handling click event:", error);
|
||||
generateIcon() {
|
||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
||||
if (imageUrl) {
|
||||
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
|
||||
} else {
|
||||
const identifier = this.contact?.did || this.entityId;
|
||||
if (!identifier) {
|
||||
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
|
||||
}
|
||||
// https://api.dicebear.com/8.x/avataaars/svg?seed=
|
||||
// ... does not render things with the same seed as this library.
|
||||
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring
|
||||
// ... which looks similar to '' at the dicebear site but which is different.
|
||||
const options: StyleOptions<object> = {
|
||||
seed: (identifier as string) || "",
|
||||
size: this.iconSize,
|
||||
};
|
||||
const avatar = createAvatar(avataaars, options);
|
||||
const svgString = avatar.toString();
|
||||
return svgString;
|
||||
}
|
||||
}
|
||||
|
||||
generateAvatarPaths(): string[] {
|
||||
if (!this.identifier) return [];
|
||||
|
||||
const options: StyleOptions<object> = {
|
||||
seed: this.identifier,
|
||||
size: this.iconSize,
|
||||
};
|
||||
const avatar = createAvatar(avataaars, options);
|
||||
const svgString = avatar.toString();
|
||||
|
||||
// Extract paths from SVG string
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgString, "image/svg+xml");
|
||||
const paths = Array.from(doc.querySelectorAll("path")).map(
|
||||
(path) => path.getAttribute("d") || "",
|
||||
);
|
||||
return paths;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.avatarPaths = this.generateAvatarPaths();
|
||||
logger.log("EntityIcon mounted, profileImageUrl:", this.profileImageUrl);
|
||||
logger.log("EntityIcon mounted, entityId:", this.entityId);
|
||||
logger.log("EntityIcon mounted, iconSize:", this.iconSize);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -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,88 +69,68 @@
|
||||
</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";
|
||||
|
||||
@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 = PlatformServiceFactory.getInstance();
|
||||
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) {
|
||||
} catch (err: unknown) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -173,7 +148,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 +162,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 +179,41 @@ 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 {
|
||||
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 {
|
||||
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 +225,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 +235,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 +250,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 +270,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 +335,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 +360,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>
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
/** * @file ProfileSection.vue * @description Component for managing user
|
||||
profile information * @author Matthew Raymer * @version 1.0.0 */
|
||||
|
||||
<template>
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||
<div v-if="loading" class="text-center mb-2">
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin text-slate-400"
|
||||
></font-awesome>
|
||||
Loading profile...
|
||||
</div>
|
||||
<div v-else class="flex items-center mb-2">
|
||||
<span class="font-bold">Public Profile</span>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||
@click="showProfileInfo"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="profileDesc"
|
||||
class="w-full h-32 p-2 border border-slate-300 rounded-md"
|
||||
placeholder="Write something about yourself for the public..."
|
||||
:readonly="loading || saving"
|
||||
:class="{ 'bg-slate-100': loading || saving }"
|
||||
></textarea>
|
||||
|
||||
<div class="flex items-center mb-4" @click="toggleLocation">
|
||||
<input v-model="includeLocation" type="checkbox" class="mr-2" />
|
||||
<label for="includeLocation">Include Location</label>
|
||||
</div>
|
||||
<div v-if="includeLocation" class="mb-4 aspect-video">
|
||||
<p class="text-sm mb-2 text-slate-500">
|
||||
For your security, choose a location nearby but not exactly at your
|
||||
place.
|
||||
</p>
|
||||
|
||||
<l-map
|
||||
ref="profileMap"
|
||||
class="!z-40 rounded-md"
|
||||
@click="handleMapClick"
|
||||
@ready="onMapReady"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
/>
|
||||
<l-marker
|
||||
v-if="latitude && longitude"
|
||||
:lat-lng="[latitude, longitude]"
|
||||
@click="confirmEraseLocation"
|
||||
/>
|
||||
</l-map>
|
||||
</div>
|
||||
<div v-if="!loading && !saving">
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loading || saving"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': loading || saving,
|
||||
}"
|
||||
@click="saveProfile"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
<button
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loading || saving"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed':
|
||||
loading || saving || (!profileDesc && !includeLocation),
|
||||
}"
|
||||
@click="confirmDeleteProfile"
|
||||
>
|
||||
Delete Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loading">Loading...</div>
|
||||
<div v-else>Saving...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
import { ProfileService } from "../services/ProfileService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
},
|
||||
})
|
||||
export default class ProfileSection extends Vue {
|
||||
@Prop({ required: true }) activeDid!: string;
|
||||
@Prop({ required: true }) partnerApiServer!: string;
|
||||
|
||||
@Emit("profile-updated") profileUpdated() {}
|
||||
|
||||
loading = true;
|
||||
saving = false;
|
||||
profileDesc = "";
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
includeLocation = false;
|
||||
zoom = 2;
|
||||
|
||||
async mounted() {
|
||||
await this.loadProfile();
|
||||
}
|
||||
|
||||
async loadProfile() {
|
||||
try {
|
||||
const profile = await ProfileService.loadProfile(
|
||||
this.activeDid,
|
||||
this.partnerApiServer,
|
||||
);
|
||||
if (profile) {
|
||||
this.profileDesc = profile.description || "";
|
||||
this.latitude = profile.location?.lat || 0;
|
||||
this.longitude = profile.location?.lng || 0;
|
||||
this.includeLocation = !!(this.latitude && this.longitude);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading profile:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Loading Profile",
|
||||
text: "Your server profile is not available.",
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveProfile() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, {
|
||||
description: this.profileDesc,
|
||||
location: this.includeLocation
|
||||
? {
|
||||
lat: this.latitude,
|
||||
lng: this.longitude,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Profile Saved",
|
||||
text: "Your profile has been updated successfully.",
|
||||
});
|
||||
this.profileUpdated();
|
||||
} catch (error) {
|
||||
logger.error("Error saving profile:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Saving Profile",
|
||||
text: "There was an error saving your profile.",
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
toggleLocation() {
|
||||
this.includeLocation = !this.includeLocation;
|
||||
if (!this.includeLocation) {
|
||||
this.latitude = 0;
|
||||
this.longitude = 0;
|
||||
this.zoom = 2;
|
||||
}
|
||||
}
|
||||
|
||||
handleMapClick(event: { latlng: { lat: number; lng: number } }) {
|
||||
this.latitude = event.latlng.lat;
|
||||
this.longitude = event.latlng.lng;
|
||||
}
|
||||
|
||||
onMapReady(map: L.Map) {
|
||||
const zoom = this.latitude && this.longitude ? 12 : 2;
|
||||
map.setView([this.latitude, this.longitude], zoom);
|
||||
}
|
||||
|
||||
confirmEraseLocation() {
|
||||
this.$notify({
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Erase Marker",
|
||||
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
||||
onYes: () => {
|
||||
this.latitude = 0;
|
||||
this.longitude = 0;
|
||||
this.zoom = 2;
|
||||
this.includeLocation = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async confirmDeleteProfile() {
|
||||
this.$notify({
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Delete Profile",
|
||||
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
|
||||
onYes: this.deleteProfile,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProfile() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer);
|
||||
this.profileDesc = "";
|
||||
this.latitude = 0;
|
||||
this.longitude = 0;
|
||||
this.includeLocation = false;
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Profile Deleted",
|
||||
text: "Your profile has been deleted successfully.",
|
||||
});
|
||||
this.profileUpdated();
|
||||
} catch (error) {
|
||||
logger.error("Error deleting profile:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Deleting Profile",
|
||||
text: "There was an error deleting your profile.",
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
showProfileInfo() {
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Public Profile Information",
|
||||
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +1,6 @@
|
||||
import BaseDexie, { Table } from "dexie";
|
||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||
import { exportDB, ExportOptions } from "dexie-export-import";
|
||||
import * as R from "ramda";
|
||||
import Dexie from "dexie";
|
||||
|
||||
import { Account, AccountsSchema } from "./tables/accounts";
|
||||
import { Contact, ContactSchema } from "./tables/contacts";
|
||||
@@ -28,26 +26,19 @@ type NonsensitiveTables = {
|
||||
};
|
||||
|
||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||
export type SecretDexie<T extends Record<string, Dexie.Table> = SecretTable> =
|
||||
BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
|
||||
export type SensitiveDexie<
|
||||
T extends Record<string, Dexie.Table> = SensitiveTables,
|
||||
> = BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
|
||||
export type NonsensitiveDexie<
|
||||
T extends Record<string, Dexie.Table> = NonsensitiveTables,
|
||||
> = BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
|
||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||
BaseDexie & T;
|
||||
|
||||
//// Initialize the DBs, starting with the sensitive ones.
|
||||
|
||||
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
||||
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
||||
secretDB.version(1).stores(SecretSchema);
|
||||
secretDB.export = (options) => exportDB(secretDB, options);
|
||||
|
||||
// Initialize Dexie database for accounts
|
||||
const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||
accountsDexie.version(1).stores(AccountsSchema);
|
||||
accountsDexie.export = (options) => exportDB(accountsDexie, options);
|
||||
|
||||
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
|
||||
// so that it's clear whether the usage needs the private key inside.
|
||||
@@ -63,15 +54,8 @@ export const accountsDBPromise = useSecretAndInitializeAccountsDB(
|
||||
|
||||
//// Now initialize the other DB.
|
||||
|
||||
// Initialize Dexie database for non-sensitive data
|
||||
// Initialize Dexie databases for non-sensitive data
|
||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||
db.version(1).stores({
|
||||
contacts: ContactSchema.contacts,
|
||||
logs: LogSchema.logs,
|
||||
settings: SettingsSchema.settings,
|
||||
temp: TempSchema.temp,
|
||||
});
|
||||
db.export = (options) => exportDB(db, options);
|
||||
|
||||
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
export interface GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
agent?: { identifier: string } | string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Interface for a Decentralized Identifier (DID)
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { KeyMeta } from "../libs/crypto/vc";
|
||||
|
||||
export interface IKey {
|
||||
id: string;
|
||||
type: string;
|
||||
controller: string;
|
||||
ethereumAddress: string;
|
||||
publicKeyHex: string;
|
||||
privateKeyHex: string;
|
||||
meta?: KeyMeta;
|
||||
}
|
||||
|
||||
export interface IService {
|
||||
id: string;
|
||||
type: string;
|
||||
serviceEndpoint: string;
|
||||
description?: string;
|
||||
metadata?: {
|
||||
version?: string;
|
||||
capabilities?: string[];
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IIdentifier {
|
||||
did: string;
|
||||
keys: IKey[];
|
||||
services: IService[];
|
||||
}
|
||||
@@ -5,4 +5,3 @@ export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
export * from "./deepLinks";
|
||||
export * from "./identifier";
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
/**
|
||||
* @file service.ts
|
||||
* @description Service interfaces for Decentralized Identifiers (DIDs)
|
||||
*
|
||||
* This module defines the service interfaces used in the TimeSafari application.
|
||||
* Services are associated with DIDs to provide additional functionality and endpoints.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Base IService interface defines common service properties
|
||||
* 2. Specialized interfaces extend IService for specific service types
|
||||
* 3. Services are stored in IIdentifier.services array
|
||||
* 4. Services are loaded and managed by PlatformServiceFactory
|
||||
*
|
||||
* Service Types:
|
||||
* - EndorserService: Handles claims and endorsements
|
||||
* - PushNotificationService: Manages web push notifications
|
||||
* - ProfileService: Handles user profiles and settings
|
||||
* - BackupService: Manages data backup and restore
|
||||
*
|
||||
* @see IIdentifier
|
||||
* @see PlatformServiceFactory
|
||||
* @see DatabaseBackupService
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base interface for all DID services
|
||||
*
|
||||
* This interface defines the core properties that all services must implement.
|
||||
* It follows the W3C DID specification for service endpoints.
|
||||
*
|
||||
* @example
|
||||
* const service: IService = {
|
||||
* id: 'endorser-service',
|
||||
* type: 'EndorserService',
|
||||
* serviceEndpoint: 'https://api.endorser.ch',
|
||||
* description: 'Endorser service for claims and endorsements',
|
||||
* metadata: {
|
||||
* version: '1.0.0',
|
||||
* capabilities: ['claims', 'endorsements'],
|
||||
* config: { apiServer: 'https://api.endorser.ch' }
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
export interface IService {
|
||||
/**
|
||||
* Unique identifier for the service
|
||||
* @example 'endorser-service'
|
||||
* @example 'push-notification-service'
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Type of service
|
||||
* @example 'EndorserService'
|
||||
* @example 'PushNotificationService'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Endpoint URL for the service
|
||||
* @example 'https://api.endorser.ch'
|
||||
* @example 'https://push.timesafari.app'
|
||||
*/
|
||||
serviceEndpoint: string;
|
||||
|
||||
/**
|
||||
* Optional human-readable description of the service
|
||||
* @example 'Service for handling claims and endorsements'
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Optional metadata for service configuration
|
||||
*/
|
||||
metadata?: {
|
||||
/**
|
||||
* Service version in semantic versioning format
|
||||
* @example '1.0.0'
|
||||
*/
|
||||
version?: string;
|
||||
|
||||
/**
|
||||
* Array of service capabilities
|
||||
* @example ['claims', 'endorsements']
|
||||
* @example ['notifications', 'alerts']
|
||||
*/
|
||||
capabilities?: string[];
|
||||
|
||||
/**
|
||||
* Service-specific configuration
|
||||
* @example { apiServer: 'https://api.endorser.ch' }
|
||||
*/
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for handling claims and endorsements
|
||||
*
|
||||
* This service provides endpoints for:
|
||||
* - Submitting claims
|
||||
* - Managing endorsements
|
||||
* - Checking rate limits
|
||||
*
|
||||
* @example
|
||||
* const endorserService: IEndorserService = {
|
||||
* id: 'endorser-service',
|
||||
* type: 'EndorserService',
|
||||
* serviceEndpoint: 'https://api.endorser.ch',
|
||||
* metadata: {
|
||||
* version: '1.0.0',
|
||||
* capabilities: ['claims', 'endorsements'],
|
||||
* config: {
|
||||
* apiServer: 'https://api.endorser.ch',
|
||||
* rateLimits: {
|
||||
* claimsPerDay: 100,
|
||||
* endorsementsPerDay: 1000
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
export interface IEndorserService extends IService {
|
||||
/** @override */
|
||||
type: "EndorserService";
|
||||
|
||||
/** @override */
|
||||
metadata: {
|
||||
version: string;
|
||||
capabilities: ["claims", "endorsements"];
|
||||
config: {
|
||||
/**
|
||||
* API server URL
|
||||
* @example 'https://api.endorser.ch'
|
||||
*/
|
||||
apiServer: string;
|
||||
|
||||
/**
|
||||
* Optional rate limits
|
||||
*/
|
||||
rateLimits?: {
|
||||
/**
|
||||
* Maximum claims per day
|
||||
* @default 100
|
||||
*/
|
||||
claimsPerDay: number;
|
||||
|
||||
/**
|
||||
* Maximum endorsements per day
|
||||
* @default 1000
|
||||
*/
|
||||
endorsementsPerDay: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for managing web push notifications
|
||||
*
|
||||
* This service provides endpoints for:
|
||||
* - Registering push subscriptions
|
||||
* - Sending push notifications
|
||||
* - Managing notification preferences
|
||||
*
|
||||
* @example
|
||||
* const pushService: IPushNotificationService = {
|
||||
* id: 'push-service',
|
||||
* type: 'PushNotificationService',
|
||||
* serviceEndpoint: 'https://push.timesafari.app',
|
||||
* metadata: {
|
||||
* version: '1.0.0',
|
||||
* capabilities: ['notifications'],
|
||||
* config: {
|
||||
* pushServer: 'https://push.timesafari.app',
|
||||
* vapidPublicKey: '...'
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
export interface IPushNotificationService extends IService {
|
||||
/** @override */
|
||||
type: "PushNotificationService";
|
||||
|
||||
/** @override */
|
||||
metadata: {
|
||||
version: string;
|
||||
capabilities: ["notifications"];
|
||||
config: {
|
||||
/**
|
||||
* Push server URL
|
||||
* @example 'https://push.timesafari.app'
|
||||
*/
|
||||
pushServer: string;
|
||||
|
||||
/**
|
||||
* Optional VAPID public key for push notifications
|
||||
*/
|
||||
vapidPublicKey?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for managing user profiles and settings
|
||||
*
|
||||
* This service provides endpoints for:
|
||||
* - Managing user profiles
|
||||
* - Updating user settings
|
||||
* - Retrieving user preferences
|
||||
*
|
||||
* @example
|
||||
* const profileService: IProfileService = {
|
||||
* id: 'profile-service',
|
||||
* type: 'ProfileService',
|
||||
* serviceEndpoint: 'https://partner-api.endorser.ch',
|
||||
* metadata: {
|
||||
* version: '1.0.0',
|
||||
* capabilities: ['profile', 'settings'],
|
||||
* config: {
|
||||
* partnerApiServer: 'https://partner-api.endorser.ch'
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
export interface IProfileService extends IService {
|
||||
/** @override */
|
||||
type: "ProfileService";
|
||||
|
||||
/** @override */
|
||||
metadata: {
|
||||
version: string;
|
||||
capabilities: ["profile", "settings"];
|
||||
config: {
|
||||
/**
|
||||
* Partner API server URL
|
||||
* @example 'https://partner-api.endorser.ch'
|
||||
*/
|
||||
partnerApiServer: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for managing data backup and restore operations
|
||||
*
|
||||
* This service provides endpoints for:
|
||||
* - Creating backups
|
||||
* - Restoring from backups
|
||||
* - Managing backup storage
|
||||
*
|
||||
* @example
|
||||
* const backupService: IBackupService = {
|
||||
* id: 'backup-service',
|
||||
* type: 'BackupService',
|
||||
* serviceEndpoint: 'https://backup.timesafari.app',
|
||||
* metadata: {
|
||||
* version: '1.0.0',
|
||||
* capabilities: ['backup', 'restore'],
|
||||
* config: {
|
||||
* storageType: 'cloud',
|
||||
* encryptionKey: '...'
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
export interface IBackupService extends IService {
|
||||
/** @override */
|
||||
type: "BackupService";
|
||||
|
||||
/** @override */
|
||||
metadata: {
|
||||
version: string;
|
||||
capabilities: ["backup", "restore"];
|
||||
config: {
|
||||
/**
|
||||
* Storage type for backups
|
||||
* @default 'local'
|
||||
*/
|
||||
storageType: "local" | "cloud";
|
||||
|
||||
/**
|
||||
* Optional encryption key for backups
|
||||
*/
|
||||
encryptionKey?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -38,10 +38,6 @@ export interface KeyMeta {
|
||||
* The Webauthn credential ID in hex, if this is from a passkey
|
||||
*/
|
||||
passkeyCredIdHex?: string;
|
||||
/**
|
||||
* The derivation path for the key
|
||||
*/
|
||||
derivationPath?: string;
|
||||
}
|
||||
|
||||
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
||||
|
||||
@@ -50,8 +50,6 @@ import {
|
||||
} from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export type { GenericVerifiableCredential, GenericCredWrapper };
|
||||
|
||||
/**
|
||||
* Standard context for schema.org data
|
||||
* @constant {string}
|
||||
|
||||
67
src/main.ts
@@ -200,63 +200,14 @@ function setupGlobalErrorHandler(app: VueApp) {
|
||||
};
|
||||
}
|
||||
|
||||
const app = createApp(App);
|
||||
const app = createApp(App)
|
||||
.component("fa", FontAwesomeIcon)
|
||||
.component("camera", Camera)
|
||||
.use(createPinia())
|
||||
.use(VueAxios, axios)
|
||||
.use(router)
|
||||
.use(Notifications);
|
||||
|
||||
// Add global error handler for component registration
|
||||
app.config.errorHandler = (err, vm, info) => {
|
||||
logger.error("Vue global error:", {
|
||||
error:
|
||||
err instanceof Error
|
||||
? {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
}
|
||||
: err,
|
||||
componentName: vm?.$options?.name || "unknown",
|
||||
info,
|
||||
componentData: vm
|
||||
? {
|
||||
hasRouter: !!vm.$router,
|
||||
hasNotify: !!vm.$notify,
|
||||
hasAxios: !!vm.axios,
|
||||
}
|
||||
: "no vm data",
|
||||
});
|
||||
};
|
||||
setupGlobalErrorHandler(app);
|
||||
|
||||
// Register components and plugins with error handling
|
||||
try {
|
||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
||||
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
logger.log("Pinia store initialized");
|
||||
|
||||
app.use(VueAxios, axios);
|
||||
logger.log("Axios initialized");
|
||||
|
||||
app.use(router);
|
||||
logger.log("Router initialized");
|
||||
|
||||
app.use(Notifications);
|
||||
logger.log("Notifications initialized");
|
||||
|
||||
setupGlobalErrorHandler(app);
|
||||
logger.log("Global error handler setup");
|
||||
|
||||
// Mount the app
|
||||
app.mount("#app");
|
||||
logger.log("App mounted successfully");
|
||||
} catch (error) {
|
||||
logger.error("Critical error during app initialization:", {
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
});
|
||||
}
|
||||
app.mount("#app");
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* @file DatabaseBackupService.ts
|
||||
* @description Capacitor-specific implementation of database backup service
|
||||
*
|
||||
* This service handles database backup operations on Capacitor platforms (Android/iOS)
|
||||
* using the Filesystem and Share plugins. It creates a temporary backup file,
|
||||
* writes the backup data to it, and shares the file using the platform's share sheet.
|
||||
*/
|
||||
|
||||
import { Filesystem, Directory } from "@capacitor/filesystem";
|
||||
import { Share } from "@capacitor/share";
|
||||
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService";
|
||||
import { log, error } from "../../utils/logger";
|
||||
|
||||
export class DatabaseBackupService extends BaseDatabaseBackupService {
|
||||
/**
|
||||
* Handles the backup process for Capacitor platforms
|
||||
*
|
||||
* @param base64Data - Backup data in base64 format
|
||||
* @param arrayBuffer - Backup data as ArrayBuffer
|
||||
* @param blob - Backup data as Blob
|
||||
*/
|
||||
protected async handleBackup(
|
||||
base64Data: string,
|
||||
_arrayBuffer: ArrayBuffer,
|
||||
_blob: Blob,
|
||||
): Promise<void> {
|
||||
try {
|
||||
log("Starting backup process for Capacitor platform");
|
||||
|
||||
// Create a timestamped backup file name
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const backupFileName = `timesafari-backup-${timestamp}.json`;
|
||||
const backupFilePath = `backups/${backupFileName}`;
|
||||
|
||||
log("Creating backup file:", {
|
||||
fileName: backupFileName,
|
||||
path: backupFilePath,
|
||||
});
|
||||
|
||||
// Write the backup file
|
||||
const writeResult = (await Filesystem.writeFile({
|
||||
path: backupFilePath,
|
||||
data: base64Data,
|
||||
directory: Directory.Cache,
|
||||
recursive: true,
|
||||
})) as unknown as { uri: string };
|
||||
|
||||
if (!writeResult.uri) {
|
||||
throw new Error("Failed to write backup file: No URI returned");
|
||||
}
|
||||
|
||||
log("Backup file written successfully:", { uri: writeResult.uri });
|
||||
|
||||
// Share the backup file
|
||||
log("Sharing backup file");
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Your TimeSafari backup file",
|
||||
url: writeResult.uri,
|
||||
dialogTitle: "Share TimeSafari Backup",
|
||||
});
|
||||
|
||||
log("Backup shared successfully");
|
||||
|
||||
// Clean up the temporary file
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: backupFilePath,
|
||||
directory: Directory.Cache,
|
||||
});
|
||||
log("Temporary backup file cleaned up");
|
||||
} catch (cleanupError) {
|
||||
error("Failed to clean up temporary backup file:", cleanupError);
|
||||
// Don't throw here as the backup was successful
|
||||
}
|
||||
} catch (err) {
|
||||
error("Error during backup process:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,8 @@ import {
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import {
|
||||
accountsDBPromise,
|
||||
retrieveSettingsForActiveAccount,
|
||||
} from "../db/index";
|
||||
import { accountsDBPromise } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Component as VueComponent } from "vue-facing-decorator";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -40,79 +35,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/account",
|
||||
name: "account",
|
||||
component: () => {
|
||||
logger.log("Starting lazy load of AccountViewView");
|
||||
return new Promise((resolve) => {
|
||||
import("../views/AccountViewView.vue")
|
||||
.then((module) => {
|
||||
if (!module?.default) {
|
||||
logger.error(
|
||||
"AccountViewView module loaded but default export is missing",
|
||||
{
|
||||
module: {
|
||||
hasDefault: !!module?.default,
|
||||
keys: Object.keys(module || {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
resolve(createErrorComponent());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the component has the required dependencies
|
||||
const component = module.default;
|
||||
logger.log("AccountViewView loaded, checking dependencies...", {
|
||||
componentName: component.name,
|
||||
hasVueComponent: component instanceof VueComponent,
|
||||
hasClass: typeof component === "function",
|
||||
type: typeof component,
|
||||
});
|
||||
|
||||
resolve(component);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to load AccountViewView:", {
|
||||
error:
|
||||
err instanceof Error
|
||||
? {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
}
|
||||
: err,
|
||||
type: typeof err,
|
||||
});
|
||||
|
||||
resolve(createErrorComponent());
|
||||
});
|
||||
});
|
||||
},
|
||||
beforeEnter: async (to, from, next) => {
|
||||
try {
|
||||
logger.log("Account route beforeEnter guard starting");
|
||||
|
||||
// Check if required dependencies are available
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
logger.log("Account route: settings loaded", {
|
||||
hasActiveDid: !!settings.activeDid,
|
||||
isRegistered: !!settings.isRegistered,
|
||||
});
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error("Error in account route beforeEnter:", {
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
});
|
||||
next({ name: "home" });
|
||||
}
|
||||
},
|
||||
component: () => import("../views/AccountViewView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/claim/:id?",
|
||||
@@ -392,271 +315,25 @@ const router = createRouter({
|
||||
// Replace initial URL to start at `/` if necessary
|
||||
router.replace(initialPath || "/");
|
||||
|
||||
// Add global error handler
|
||||
router.onError((error, to, from) => {
|
||||
logger.error("Router error:", {
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
to: {
|
||||
name: to.name,
|
||||
path: to.path,
|
||||
},
|
||||
from: {
|
||||
name: from.name,
|
||||
path: from.path,
|
||||
},
|
||||
});
|
||||
const errorHandler = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any,
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
) => {
|
||||
// Handle the error here
|
||||
logger.error("Caught in top level error handler:", error, to, from);
|
||||
alert("Something is very wrong. Try reloading or restarting the app.");
|
||||
|
||||
// If it's a reference error during account view import, try to handle it gracefully
|
||||
if (error instanceof ReferenceError && to.name === "account") {
|
||||
logger.error("Account view import error:", {
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
});
|
||||
// Instead of redirecting, let the component's error handling take over
|
||||
return;
|
||||
}
|
||||
});
|
||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||
};
|
||||
|
||||
// Add navigation guard for debugging
|
||||
router.beforeEach((to, from, next) => {
|
||||
logger.log("Navigation debug:", {
|
||||
to: {
|
||||
fullPath: to.fullPath,
|
||||
path: to.path,
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
},
|
||||
from: {
|
||||
fullPath: from.fullPath,
|
||||
path: from.path,
|
||||
name: from.name,
|
||||
params: from.params,
|
||||
query: from.query,
|
||||
},
|
||||
});
|
||||
router.onError(errorHandler); // Assign the error handler to the router instance
|
||||
|
||||
// For account route, try to preload the component
|
||||
if (to.name === "account") {
|
||||
logger.log("Preloading account component...");
|
||||
|
||||
// Wrap in try-catch and use Promise
|
||||
new Promise((resolve) => {
|
||||
logger.log("Starting dynamic import of AccountViewView");
|
||||
|
||||
// Add immediate try-catch to get more context
|
||||
try {
|
||||
const importPromise = import("../views/AccountViewView.vue");
|
||||
logger.log("Import initiated successfully");
|
||||
|
||||
importPromise
|
||||
.then((module) => {
|
||||
try {
|
||||
logger.log("Import completed, analyzing module:", {
|
||||
moduleExists: !!module,
|
||||
moduleType: typeof module,
|
||||
moduleKeys: Object.keys(module || {}),
|
||||
hasDefault: !!module?.default,
|
||||
defaultType: module?.default
|
||||
? typeof module.default
|
||||
: "undefined",
|
||||
defaultConstructor: module?.default?.constructor?.name,
|
||||
moduleContent: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(module).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === "function"
|
||||
? "function"
|
||||
: typeof value === "object"
|
||||
? Object.keys(value || {})
|
||||
: typeof value,
|
||||
]),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
if (!module?.default) {
|
||||
logger.error(
|
||||
"AccountViewView preload: module loaded but default export is missing",
|
||||
{
|
||||
module: {
|
||||
hasDefault: !!module?.default,
|
||||
keys: Object.keys(module || {}),
|
||||
moduleType: typeof module,
|
||||
exports: Object.keys(module || {}).map((key) => ({
|
||||
key,
|
||||
type: typeof (module as any)[key],
|
||||
value:
|
||||
typeof (module as any)[key] === "function"
|
||||
? "function"
|
||||
: typeof (module as any)[key] === "object"
|
||||
? Object.keys((module as any)[key] || {})
|
||||
: (module as any)[key],
|
||||
})),
|
||||
},
|
||||
},
|
||||
);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const component = module.default;
|
||||
|
||||
// Try to safely inspect the component
|
||||
const componentDetails = {
|
||||
componentName: component.name,
|
||||
hasVueComponent: component instanceof VueComponent,
|
||||
hasClass: typeof component === "function",
|
||||
type: typeof component,
|
||||
properties: Object.keys(component),
|
||||
decorators: Object.getOwnPropertyDescriptor(
|
||||
component,
|
||||
"__decorators",
|
||||
),
|
||||
vueOptions:
|
||||
(component as any).__vccOpts ||
|
||||
(component as any).options ||
|
||||
null,
|
||||
setup: typeof (component as any).setup === "function",
|
||||
render: typeof (component as any).render === "function",
|
||||
components: (component as any).components
|
||||
? Object.keys((component as any).components)
|
||||
: null,
|
||||
imports: Object.keys(module).filter((key) => key !== "default"),
|
||||
};
|
||||
|
||||
logger.log("Successfully analyzed component:", componentDetails);
|
||||
resolve(component);
|
||||
} catch (analysisError) {
|
||||
logger.error("Error during component analysis:", {
|
||||
error:
|
||||
analysisError instanceof Error
|
||||
? {
|
||||
name: analysisError.name,
|
||||
message: analysisError.message,
|
||||
stack: analysisError.stack,
|
||||
keys: Object.keys(analysisError),
|
||||
properties: Object.getOwnPropertyNames(analysisError),
|
||||
}
|
||||
: analysisError,
|
||||
type: typeof analysisError,
|
||||
phase: "analysis",
|
||||
});
|
||||
resolve(null);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to preload account component:", {
|
||||
error:
|
||||
err instanceof Error
|
||||
? {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
keys: Object.keys(err),
|
||||
properties: Object.getOwnPropertyNames(err),
|
||||
}
|
||||
: err,
|
||||
type: typeof err,
|
||||
context: {
|
||||
routeName: to.name,
|
||||
routePath: to.path,
|
||||
fromRoute: from.name,
|
||||
},
|
||||
phase: "module-load",
|
||||
});
|
||||
resolve(null);
|
||||
});
|
||||
} catch (immediateError) {
|
||||
logger.error("Immediate error during import initiation:", {
|
||||
error:
|
||||
immediateError instanceof Error
|
||||
? {
|
||||
name: immediateError.name,
|
||||
message: immediateError.message,
|
||||
stack: immediateError.stack,
|
||||
keys: Object.keys(immediateError),
|
||||
properties: Object.getOwnPropertyNames(immediateError),
|
||||
}
|
||||
: immediateError,
|
||||
type: typeof immediateError,
|
||||
context: {
|
||||
routeName: to.name,
|
||||
routePath: to.path,
|
||||
fromRoute: from.name,
|
||||
importPath: "../views/AccountViewView.vue",
|
||||
},
|
||||
phase: "import",
|
||||
});
|
||||
resolve(null);
|
||||
}
|
||||
}).catch((err) => {
|
||||
logger.error("Critical error in account component preload:", {
|
||||
error:
|
||||
err instanceof Error
|
||||
? {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
}
|
||||
: err,
|
||||
context: {
|
||||
routeName: to.name,
|
||||
routePath: to.path,
|
||||
fromRoute: from.name,
|
||||
},
|
||||
phase: "wrapper",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Always call next() to continue navigation
|
||||
next();
|
||||
});
|
||||
|
||||
function createErrorComponent() {
|
||||
return defineComponent({
|
||||
name: "AccountViewError",
|
||||
components: {
|
||||
// Add any required components here
|
||||
},
|
||||
setup() {
|
||||
const goHome = () => {
|
||||
router.push({ name: "home" });
|
||||
};
|
||||
|
||||
return {
|
||||
goHome,
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<section class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<h1 class="text-4xl text-center font-light mb-8">Error Loading Account View</h1>
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
|
||||
<strong class="font-bold">Failed to load account view.</strong>
|
||||
<span class="block sm:inline"> Please try refreshing the page.</span>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<button @click="goHome" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Return to Home
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
});
|
||||
}
|
||||
// router.beforeEach((to, from, next) => {
|
||||
// console.log("Navigating to view:", to.name);
|
||||
// console.log("From view:", from.name);
|
||||
// next();
|
||||
// });
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* @file DatabaseBackupService.ts
|
||||
* @description Base service class for handling database backup operations
|
||||
*
|
||||
* This service implements the Template Method pattern to provide a common interface
|
||||
* for database backup operations across different platforms. It defines the structure
|
||||
* of backup operations while delegating platform-specific implementations to subclasses.
|
||||
*
|
||||
* Build Process Integration:
|
||||
* 1. Platform-Specific Implementation:
|
||||
* - Each platform (web, electron, capacitor) has its own implementation
|
||||
* - Implementations are loaded dynamically via PlatformServiceFactory
|
||||
* - Located in ./platforms/{platform}/DatabaseBackupService.ts
|
||||
*
|
||||
* 2. Build Configuration:
|
||||
* - Vite config files (vite.config.*.mts) set VITE_PLATFORM
|
||||
* - PlatformServiceFactory uses this to load correct implementation
|
||||
* - Build process creates separate chunks for each platform
|
||||
*
|
||||
* 3. Data Handling:
|
||||
* - Supports multiple data formats (base64, ArrayBuffer, Blob)
|
||||
* - Platform implementations handle format conversion
|
||||
* - Ensures consistent backup format across platforms
|
||||
*
|
||||
* Usage:
|
||||
* - Create backup: DatabaseBackupService.createAndShareBackup(data)
|
||||
* - Platform-specific: new WebDatabaseBackupService().handleBackup()
|
||||
*
|
||||
* @see PlatformServiceFactory.ts
|
||||
* @see vite.config.web.mts
|
||||
* @see vite.config.electron.mts
|
||||
* @see vite.config.capacitor.mts
|
||||
*/
|
||||
|
||||
import { PlatformServiceFactory } from "./PlatformServiceFactory";
|
||||
import { log, error } from "../utils/logger";
|
||||
|
||||
export class DatabaseBackupService {
|
||||
/**
|
||||
* Template method that must be implemented by platform-specific services
|
||||
* @param base64Data - Backup data in base64 format
|
||||
* @param arrayBuffer - Backup data as ArrayBuffer
|
||||
* @param blob - Backup data as Blob
|
||||
* @throws Error if not implemented by subclass
|
||||
*/
|
||||
protected async handleBackup(
|
||||
_base64Data: string,
|
||||
_arrayBuffer: ArrayBuffer,
|
||||
_blob: Blob,
|
||||
): Promise<void> {
|
||||
throw new Error(
|
||||
"handleBackup must be implemented by platform-specific service",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create and share a backup
|
||||
* Uses PlatformServiceFactory to get platform-specific implementation
|
||||
*
|
||||
* @param base64Data - Backup data in base64 format
|
||||
* @param arrayBuffer - Backup data as ArrayBuffer
|
||||
* @param blob - Backup data as Blob
|
||||
* @returns Promise that resolves when backup is complete
|
||||
*/
|
||||
public static async createAndShareBackup(
|
||||
base64Data: string,
|
||||
arrayBuffer: ArrayBuffer,
|
||||
blob: Blob,
|
||||
): Promise<void> {
|
||||
try {
|
||||
log("Creating platform-specific backup service");
|
||||
const backupService = await this.getPlatformSpecificBackupService();
|
||||
log("Backup service created successfully");
|
||||
|
||||
log("Executing platform-specific backup");
|
||||
await backupService.handleBackup(base64Data, arrayBuffer, blob);
|
||||
log("Backup completed successfully");
|
||||
} catch (err) {
|
||||
error("Error during backup creation:", err);
|
||||
if (err instanceof Error) {
|
||||
error("Error details:", {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private static async getPlatformSpecificBackupService(): Promise<DatabaseBackupService> {
|
||||
const factory = PlatformServiceFactory.getInstance();
|
||||
return await factory.createDatabaseBackupService();
|
||||
}
|
||||
}
|
||||
101
src/services/PlatformService.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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 capabilities interface defining what features are available
|
||||
* on the current platform implementation
|
||||
*/
|
||||
export interface PlatformCapabilities {
|
||||
/** Whether the platform supports native file system access */
|
||||
hasFileSystem: boolean;
|
||||
/** Whether the platform supports native camera access */
|
||||
hasCamera: boolean;
|
||||
/** Whether the platform is a mobile device */
|
||||
isMobile: boolean;
|
||||
/** Whether the platform is iOS specifically */
|
||||
isIOS: boolean;
|
||||
/** Whether the platform supports native file download */
|
||||
hasFileDownload: boolean;
|
||||
/** Whether the platform requires special file handling instructions */
|
||||
needsFileHandlingInstructions: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-agnostic interface for handling platform-specific operations.
|
||||
* Provides a common API for file system operations, camera interactions,
|
||||
* and platform detection across different platforms (web, mobile, desktop).
|
||||
*/
|
||||
export interface PlatformService {
|
||||
// Platform capabilities
|
||||
/**
|
||||
* Gets the current platform's capabilities
|
||||
* @returns Object describing what features are available on this platform
|
||||
*/
|
||||
getCapabilities(): PlatformCapabilities;
|
||||
|
||||
// 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>;
|
||||
|
||||
/**
|
||||
* Writes content to a file at the specified path and shares it.
|
||||
* @param fileName - The filename of the file to write
|
||||
* @param content - The content to write to the file
|
||||
* @returns Promise that resolves when the write is complete
|
||||
*/
|
||||
writeAndShareFile(fileName: 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[]>;
|
||||
|
||||
// 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>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
@@ -1,195 +1,58 @@
|
||||
/**
|
||||
* @file PlatformServiceFactory.ts
|
||||
* @description Factory for creating platform-specific service implementations
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*
|
||||
* This factory implements the Abstract Factory pattern to create platform-specific
|
||||
* implementations of services. It uses Vite's dynamic import feature to load the
|
||||
* appropriate implementation based on the current platform (web, electron, etc.).
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Singleton Pattern:
|
||||
* - Ensures only one factory instance exists
|
||||
* - Manages platform-specific service instances
|
||||
* - Maintains consistent state across the application
|
||||
*
|
||||
* 2. Dynamic Loading:
|
||||
* - Uses Vite's dynamic import for platform-specific code
|
||||
* - Loads services on-demand based on platform
|
||||
* - Handles platform detection and service instantiation
|
||||
*
|
||||
* 3. Platform Detection:
|
||||
* - Uses VITE_PLATFORM environment variable
|
||||
* - Supports web, electron, and capacitor platforms
|
||||
* - Falls back to 'web' if platform is not specified
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* // Get factory instance
|
||||
* const factory = PlatformServiceFactory.getInstance();
|
||||
*
|
||||
* // Create platform-specific service
|
||||
* const backupService = await factory.createDatabaseBackupService();
|
||||
* ```
|
||||
*
|
||||
* @see vite.config.web.mts
|
||||
* @see vite.config.electron.mts
|
||||
* @see vite.config.capacitor.mts
|
||||
* @see DatabaseBackupService
|
||||
*/
|
||||
|
||||
import { DatabaseBackupService } from "./DatabaseBackupService";
|
||||
import { DatabaseBackupService as StubDatabaseBackupService } from "./platforms/empty";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformService } from "./PlatformService";
|
||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
||||
|
||||
/**
|
||||
* Factory class for creating platform-specific service implementations
|
||||
* Factory class for creating platform-specific service implementations.
|
||||
* Implements the Singleton pattern to ensure only one instance of PlatformService exists.
|
||||
*
|
||||
* This class manages the creation and instantiation of platform-specific
|
||||
* service implementations. It uses the Abstract Factory pattern to provide
|
||||
* a consistent interface for creating services across different platforms.
|
||||
* 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
|
||||
* // Get factory instance
|
||||
* const factory = PlatformServiceFactory.getInstance();
|
||||
*
|
||||
* // Create platform-specific service
|
||||
* const backupService = await factory.createDatabaseBackupService();
|
||||
*
|
||||
* // Use the service
|
||||
* await backupService.handleBackup(data);
|
||||
* const platformService = PlatformServiceFactory.getInstance();
|
||||
* await platformService.takePicture();
|
||||
* ```
|
||||
*/
|
||||
export class PlatformServiceFactory {
|
||||
/**
|
||||
* Singleton instance of the factory
|
||||
* @private
|
||||
*/
|
||||
private static instance: PlatformServiceFactory;
|
||||
private static instance: PlatformService | null = null;
|
||||
|
||||
/**
|
||||
* Current platform identifier
|
||||
* @private
|
||||
* Gets or creates the singleton instance of PlatformService.
|
||||
* Creates the appropriate platform-specific implementation based on environment.
|
||||
*
|
||||
* @returns {PlatformService} The singleton instance of PlatformService
|
||||
*/
|
||||
private platform: string;
|
||||
|
||||
/**
|
||||
* Private constructor to enforce singleton pattern
|
||||
*
|
||||
* Initializes the factory with the current platform from environment variables.
|
||||
* Falls back to 'web' if no platform is specified.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private constructor() {
|
||||
this.platform = import.meta.env.VITE_PLATFORM || "web";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of the factory
|
||||
*
|
||||
* Creates a new instance if one doesn't exist, otherwise returns
|
||||
* the existing instance.
|
||||
*
|
||||
* @returns {PlatformServiceFactory} The singleton factory instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const factory = PlatformServiceFactory.getInstance();
|
||||
* ```
|
||||
*/
|
||||
public static getInstance(): PlatformServiceFactory {
|
||||
if (!PlatformServiceFactory.instance) {
|
||||
PlatformServiceFactory.instance = new PlatformServiceFactory();
|
||||
public static getInstance(): PlatformService {
|
||||
if (PlatformServiceFactory.instance) {
|
||||
return PlatformServiceFactory.instance;
|
||||
}
|
||||
|
||||
const platform = process.env.VITE_PLATFORM || "web";
|
||||
|
||||
switch (platform) {
|
||||
case "capacitor":
|
||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
||||
break;
|
||||
case "electron":
|
||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
||||
break;
|
||||
case "pywebview":
|
||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
||||
break;
|
||||
case "web":
|
||||
default:
|
||||
PlatformServiceFactory.instance = new WebPlatformService();
|
||||
break;
|
||||
}
|
||||
|
||||
return PlatformServiceFactory.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a platform-specific database backup service
|
||||
*
|
||||
* Dynamically loads and instantiates the appropriate implementation
|
||||
* based on the current platform. The implementation is loaded from
|
||||
* the platforms/{platform}/DatabaseBackupService.ts file.
|
||||
*
|
||||
* @returns {Promise<DatabaseBackupService>} A promise that resolves to a platform-specific backup service
|
||||
* @throws {Error} If the service fails to load or instantiate
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const factory = PlatformServiceFactory.getInstance();
|
||||
* try {
|
||||
* const backupService = await factory.createDatabaseBackupService();
|
||||
* await backupService.handleBackup(data);
|
||||
* } catch (error) {
|
||||
* logger.error('Failed to create backup service:', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public async createDatabaseBackupService(): Promise<DatabaseBackupService> {
|
||||
// List of supported platforms for web builds
|
||||
const webSupportedPlatforms = ["web", "capacitor", "electron"];
|
||||
|
||||
// Return stub implementation for unsupported platforms
|
||||
if (!webSupportedPlatforms.includes(this.platform)) {
|
||||
logger.log(
|
||||
`Using stub implementation for unsupported platform: ${this.platform}`,
|
||||
);
|
||||
return new StubDatabaseBackupService();
|
||||
}
|
||||
|
||||
try {
|
||||
logger.log(`Loading platform-specific service for ${this.platform}`);
|
||||
// Use dynamic import with platform-specific path
|
||||
const module = await import(
|
||||
/* @vite-ignore */
|
||||
`./platforms/${this.platform}/DatabaseBackupService.ts`
|
||||
);
|
||||
logger.log("Platform service loaded successfully");
|
||||
return new module.DatabaseBackupService();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[TimeSafari] Failed to load platform-specific service for ${this.platform}:`,
|
||||
error,
|
||||
);
|
||||
// Fallback to stub implementation on error
|
||||
logger.log("Falling back to stub implementation");
|
||||
return new StubDatabaseBackupService();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current platform identifier
|
||||
*
|
||||
* @returns {string} The current platform identifier
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const factory = PlatformServiceFactory.getInstance();
|
||||
* logger.log(factory.getPlatform()); // 'web', 'electron', or 'capacitor'
|
||||
* ```
|
||||
*/
|
||||
public getPlatform(): string {
|
||||
return this.platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current platform identifier
|
||||
*
|
||||
* This method is primarily used for testing purposes to override
|
||||
* the platform detection. Use with caution in production code.
|
||||
*
|
||||
* @param {string} platform - The platform identifier to set
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const factory = PlatformServiceFactory.getInstance();
|
||||
* factory.setPlatform('electron'); // For testing purposes only
|
||||
* ```
|
||||
*/
|
||||
public setPlatform(platform: string): void {
|
||||
this.platform = platform;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* @file ProfileService.ts
|
||||
* @description Service class for handling user profile operations
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { getHeaders } from "../libs/endorserServer";
|
||||
import type { UserProfile } from "@/types/interfaces";
|
||||
|
||||
export class ProfileService {
|
||||
/**
|
||||
* Saves a user profile to the server
|
||||
* @param activeDid - The user's active DID
|
||||
* @param partnerApiServer - The partner API server URL
|
||||
* @param profile - The profile data to save
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
static async saveProfile(
|
||||
activeDid: string,
|
||||
partnerApiServer: string,
|
||||
profile: Partial<UserProfile>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const response = await fetch(
|
||||
`${partnerApiServer}/api/partner/userProfile`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(profile),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save profile: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error saving profile:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user profile from the server
|
||||
* @param activeDid - The user's active DID
|
||||
* @param partnerApiServer - The partner API server URL
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
static async deleteProfile(
|
||||
activeDid: string,
|
||||
partnerApiServer: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const response = await fetch(
|
||||
`${partnerApiServer}/api/partner/userProfile`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete profile: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error deleting profile:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a user profile from the server
|
||||
* @param activeDid - The user's active DID
|
||||
* @param partnerApiServer - The partner API server URL
|
||||
* @returns Promise<UserProfile | null>
|
||||
*/
|
||||
static async loadProfile(
|
||||
activeDid: string,
|
||||
partnerApiServer: string,
|
||||
): Promise<UserProfile | null> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const response = await fetch(
|
||||
`${partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load profile: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
logger.error("Error loading profile:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* @file RateLimitsService.ts
|
||||
* @description Service class for handling rate limit operations
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { getHeaders } from "../libs/endorserServer";
|
||||
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
|
||||
import axios from "axios";
|
||||
|
||||
export class RateLimitsService {
|
||||
/**
|
||||
* Fetches rate limits for a given DID
|
||||
* @param apiServer - The API server URL
|
||||
* @param did - The user's DID
|
||||
* @returns Promise<EndorserRateLimits>
|
||||
*/
|
||||
static async fetchRateLimits(
|
||||
apiServer: string,
|
||||
did: string,
|
||||
): Promise<EndorserRateLimits> {
|
||||
logger.log("Fetching rate limits for DID:", did);
|
||||
logger.log("Using API server:", apiServer);
|
||||
|
||||
try {
|
||||
const headers = await getHeaders(did);
|
||||
const response = await axios.get(
|
||||
`${apiServer}/api/v2/rate-limits/${did}`,
|
||||
{ headers },
|
||||
);
|
||||
logger.log("Rate limits response:", response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
(error.response?.status === 400 || error.response?.status === 404)
|
||||
) {
|
||||
const errorData = error.response.data as {
|
||||
error?: { message?: string; code?: string };
|
||||
};
|
||||
if (
|
||||
errorData.error?.code === "UNREGISTERED_USER" ||
|
||||
error.response?.status === 404
|
||||
) {
|
||||
logger.log("User is not registered, returning default limits");
|
||||
return {
|
||||
doneClaimsThisWeek: "0",
|
||||
maxClaimsPerWeek: "0",
|
||||
nextWeekBeginDateTime: new Date().toISOString(),
|
||||
doneRegistrationsThisMonth: "0",
|
||||
maxRegistrationsPerMonth: "0",
|
||||
nextMonthBeginDateTime: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
logger.error("Error fetching rate limits:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches image rate limits for a given DID
|
||||
* @param apiServer - The API server URL
|
||||
* @param activeDid - The user's active DID
|
||||
* @returns Promise<ImageRateLimits>
|
||||
*/
|
||||
static async fetchImageRateLimits(
|
||||
apiServer: string,
|
||||
activeDid: string,
|
||||
): Promise<ImageRateLimits> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const response = await fetch(
|
||||
`${apiServer}/api/endorser/imageRateLimits/${activeDid}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch image rate limits: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
logger.error("Error fetching image rate limits:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats rate limit error messages
|
||||
* @param error - The error object
|
||||
* @returns string
|
||||
*/
|
||||
static formatRateLimitError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const err = error as {
|
||||
response?: { data?: { error?: { message?: string } } };
|
||||
};
|
||||
return err.response?.data?.error?.message || "An unknown error occurred";
|
||||
}
|
||||
return "An unknown error occurred";
|
||||
}
|
||||
}
|
||||
@@ -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}:`, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
473
src/services/platforms/CapacitorPlatformService.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||
import { Share } from "@capacitor/share";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* Gets the capabilities of the Capacitor platform
|
||||
* @returns Platform capabilities object
|
||||
*/
|
||||
getCapabilities(): PlatformCapabilities {
|
||||
return {
|
||||
hasFileSystem: true,
|
||||
hasCamera: true,
|
||||
isMobile: true,
|
||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
hasFileDownload: false,
|
||||
needsFileHandlingInstructions: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and requests storage permissions if needed
|
||||
* @returns Promise that resolves when permissions are granted
|
||||
* @throws Error if permissions are denied
|
||||
*/
|
||||
private async checkStoragePermissions(): Promise<void> {
|
||||
try {
|
||||
const logData = {
|
||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.log(
|
||||
"Checking storage permissions",
|
||||
JSON.stringify(logData, null, 2),
|
||||
);
|
||||
|
||||
if (this.getCapabilities().isIOS) {
|
||||
// iOS uses different permission model
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to access a test directory to check permissions
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: "/storage/emulated/0/Download",
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
logger.log(
|
||||
"Storage permissions already granted",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const errorLogData = {
|
||||
error: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// "File does not exist" is expected and not a permission error
|
||||
if (err.message === "File does not exist") {
|
||||
logger.log(
|
||||
"Directory does not exist (expected), proceeding with write",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for actual permission errors
|
||||
if (
|
||||
err.message.includes("permission") ||
|
||||
err.message.includes("access")
|
||||
) {
|
||||
logger.log(
|
||||
"Permission check failed, requesting permissions",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
|
||||
// The Filesystem plugin will automatically request permissions when needed
|
||||
// We just need to try the operation again
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: "/storage/emulated/0/Download",
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
logger.log(
|
||||
"Storage permissions granted after request",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
return;
|
||||
} catch (retryError: unknown) {
|
||||
const retryErr = retryError as Error;
|
||||
throw new Error(
|
||||
`Failed to obtain storage permissions: ${retryErr.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// For any other error, log it but don't treat as permission error
|
||||
logger.log(
|
||||
"Unexpected error during permission check",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const errorLogData = {
|
||||
error: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.error(
|
||||
"Error checking/requesting permissions",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
throw new Error(`Failed to obtain storage permissions: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 safe storage and offers sharing.
|
||||
*
|
||||
* Platform-specific behavior:
|
||||
* - Saves to app's Documents directory
|
||||
* - Offers sharing functionality to move file elsewhere
|
||||
*
|
||||
* The method handles:
|
||||
* 1. Writing to app-safe storage
|
||||
* 2. Sharing the file with user's preferred app
|
||||
* 3. Error handling and logging
|
||||
*
|
||||
* @param fileName - The name of the file to create (e.g. "backup.json")
|
||||
* @param content - The content to write to the file
|
||||
*
|
||||
* @throws Error if:
|
||||
* - File writing fails
|
||||
* - Sharing fails
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Save and share a JSON file
|
||||
* await platformService.writeFile(
|
||||
* "backup.json",
|
||||
* JSON.stringify(data)
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async writeFile(fileName: string, content: string): Promise<void> {
|
||||
try {
|
||||
const logData = {
|
||||
targetFileName: fileName,
|
||||
contentLength: content.length,
|
||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.log(
|
||||
"Starting writeFile operation",
|
||||
JSON.stringify(logData, null, 2),
|
||||
);
|
||||
|
||||
// For Android, we need to handle content URIs differently
|
||||
if (this.getCapabilities().isIOS) {
|
||||
// Write to app's Documents directory for iOS
|
||||
const writeResult = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
const writeSuccessLogData = {
|
||||
path: writeResult.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.log(
|
||||
"File write successful",
|
||||
JSON.stringify(writeSuccessLogData, null, 2),
|
||||
);
|
||||
|
||||
// Offer to share the file
|
||||
try {
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Here is your TimeSafari backup file.",
|
||||
url: writeResult.uri,
|
||||
dialogTitle: "Share your backup",
|
||||
});
|
||||
|
||||
logger.log(
|
||||
"Share dialog shown",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
} catch (shareError) {
|
||||
// Log share error but don't fail the operation
|
||||
logger.error(
|
||||
"Share dialog failed",
|
||||
JSON.stringify(
|
||||
{
|
||||
error: shareError,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For Android, first write to app's Documents directory
|
||||
const writeResult = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
const writeSuccessLogData = {
|
||||
path: writeResult.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.log(
|
||||
"File write successful to app storage",
|
||||
JSON.stringify(writeSuccessLogData, null, 2),
|
||||
);
|
||||
|
||||
// Then share the file to let user choose where to save it
|
||||
try {
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Here is your TimeSafari backup file.",
|
||||
url: writeResult.uri,
|
||||
dialogTitle: "Save your backup",
|
||||
});
|
||||
|
||||
logger.log(
|
||||
"Share dialog shown for Android",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
} catch (shareError) {
|
||||
// Log share error but don't fail the operation
|
||||
logger.error(
|
||||
"Share dialog failed for Android",
|
||||
JSON.stringify(
|
||||
{
|
||||
error: shareError,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const finalErrorLogData = {
|
||||
error: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.error(
|
||||
"Error in writeFile operation:",
|
||||
JSON.stringify(finalErrorLogData, null, 2),
|
||||
);
|
||||
throw new Error(`Failed to save file: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to a file in the device's app-private storage.
|
||||
* Then shares the file using the system share dialog.
|
||||
*
|
||||
* Works on both Android and iOS without needing external storage permissions.
|
||||
*
|
||||
* @param fileName - The name of the file to create (e.g. "backup.json")
|
||||
* @param content - The content to write to the file
|
||||
*/
|
||||
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logData = {
|
||||
action: 'writeAndShareFile',
|
||||
fileName,
|
||||
contentLength: content.length,
|
||||
timestamp,
|
||||
};
|
||||
logger.log('[CapacitorPlatformService]', JSON.stringify(logData, null, 2));
|
||||
|
||||
try {
|
||||
const { uri } = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
logger.log('[CapacitorPlatformService] File write successful:', { uri, timestamp: new Date().toISOString() });
|
||||
|
||||
await Share.share({
|
||||
title: 'TimeSafari Backup',
|
||||
text: 'Here is your backup file.',
|
||||
url: uri,
|
||||
dialogTitle: 'Share your backup file',
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const errLog = {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
logger.error('[CapacitorPlatformService] Error writing or sharing file:', JSON.stringify(errLog, null, 2));
|
||||
throw new Error(`Failed to write or share file: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
111
src/services/platforms/ElectronPlatformService.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} 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 {
|
||||
/**
|
||||
* Gets the capabilities of the Electron platform
|
||||
* @returns Platform capabilities object
|
||||
*/
|
||||
getCapabilities(): PlatformCapabilities {
|
||||
return {
|
||||
hasFileSystem: false, // Not implemented yet
|
||||
hasCamera: false, // Not implemented yet
|
||||
isMobile: false,
|
||||
isIOS: false,
|
||||
hasFileDownload: false, // Not implemented yet
|
||||
needsFileHandlingInstructions: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
112
src/services/platforms/PyWebViewPlatformService.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} 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 {
|
||||
/**
|
||||
* Gets the capabilities of the PyWebView platform
|
||||
* @returns Platform capabilities object
|
||||
*/
|
||||
getCapabilities(): PlatformCapabilities {
|
||||
return {
|
||||
hasFileSystem: false, // Not implemented yet
|
||||
hasCamera: false, // Not implemented yet
|
||||
isMobile: false,
|
||||
isIOS: false,
|
||||
hasFileDownload: false, // Not implemented yet
|
||||
needsFileHandlingInstructions: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
231
src/services/platforms/WebPlatformService.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} 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 {
|
||||
/**
|
||||
* Gets the capabilities of the web platform
|
||||
* @returns Platform capabilities object
|
||||
*/
|
||||
getCapabilities(): PlatformCapabilities {
|
||||
return {
|
||||
hasFileSystem: false,
|
||||
hasCamera: true, // Through file input with capture
|
||||
isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent),
|
||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
hasFileDownload: true,
|
||||
needsFileHandlingInstructions: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @file DatabaseBackupService.ts
|
||||
* @description Capacitor-specific implementation of the DatabaseBackupService
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||
import { Filesystem } from "@capacitor/filesystem";
|
||||
import { Share } from "@capacitor/share";
|
||||
|
||||
export default class CapacitorDatabaseBackupService extends DatabaseBackupService {
|
||||
protected async handleBackup(
|
||||
base64Data: string,
|
||||
_arrayBuffer: ArrayBuffer,
|
||||
_blob: Blob,
|
||||
): Promise<void> {
|
||||
// Capacitor platform handling
|
||||
const fileName = `database-backup-${new Date().toISOString()}.json`;
|
||||
const path = `backups/${fileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path,
|
||||
data: base64Data,
|
||||
directory: "CACHE",
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
await Share.share({
|
||||
title: "Database Backup",
|
||||
text: "Here's your database backup",
|
||||
url: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||
import { dialog } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
export default class ElectronDatabaseBackupService extends DatabaseBackupService {
|
||||
protected async handleBackup(base64Data: string): Promise<void> {
|
||||
const { filePath } = await dialog.showSaveDialog({
|
||||
title: "Save Database Backup",
|
||||
defaultPath: path.join(process.env.HOME || "", "database-backup.json"),
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
fs.writeFileSync(filePath, base64Data, "base64");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* @file empty.ts
|
||||
* @description Stub implementation for excluding platform-specific code
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { DatabaseBackupService } from "../DatabaseBackupService";
|
||||
|
||||
export default class StubDatabaseBackupService extends DatabaseBackupService {
|
||||
protected async handleBackup(
|
||||
_base64Data: string,
|
||||
_arrayBuffer: ArrayBuffer,
|
||||
_blob: Blob,
|
||||
): Promise<void> {
|
||||
throw new Error("This platform does not support database backups");
|
||||
}
|
||||
}
|
||||
|
||||
export { StubDatabaseBackupService as DatabaseBackupService };
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @file DatabaseBackupService.ts
|
||||
* @description Web-specific implementation of the DatabaseBackupService
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||
import { log, error } from "../../../utils/logger";
|
||||
|
||||
export default class WebDatabaseBackupService extends DatabaseBackupService {
|
||||
protected async handleBackup(
|
||||
_base64Data: string,
|
||||
_arrayBuffer: ArrayBuffer,
|
||||
blob: Blob,
|
||||
): Promise<void> {
|
||||
try {
|
||||
log("Starting web platform backup");
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `database-backup-${new Date().toISOString()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
log("Web platform backup completed");
|
||||
} catch (err) {
|
||||
error("Error during web platform backup:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/types/capacitor.d.ts
vendored
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Type declarations for Capacitor modules used in the application.
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
declare module "@capacitor/filesystem" {
|
||||
export interface FileWriteOptions {
|
||||
path: string;
|
||||
data: string;
|
||||
directory?: string;
|
||||
encoding?: string;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface FileReadResult {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface FileDeleteOptions {
|
||||
path: string;
|
||||
directory?: string;
|
||||
}
|
||||
|
||||
export interface FilesystemDirectory {
|
||||
Cache: "CACHE";
|
||||
Documents: "DOCUMENTS";
|
||||
Data: "DATA";
|
||||
External: "EXTERNAL";
|
||||
ExternalStorage: "EXTERNAL_STORAGE";
|
||||
}
|
||||
|
||||
export interface Filesystem {
|
||||
writeFile(options: FileWriteOptions): Promise<void>;
|
||||
readFile(options: {
|
||||
path: string;
|
||||
directory?: string;
|
||||
}): Promise<FileReadResult>;
|
||||
deleteFile(options: FileDeleteOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export const Filesystem: Filesystem;
|
||||
export const Directory: FilesystemDirectory;
|
||||
export const Encoding: {
|
||||
UTF8: "utf8";
|
||||
ASCII: "ascii";
|
||||
UTF16: "utf16";
|
||||
};
|
||||
}
|
||||
|
||||
declare module "@capacitor/share" {
|
||||
export interface ShareOptions {
|
||||
title?: string;
|
||||
text?: string;
|
||||
url?: string;
|
||||
dialogTitle?: string;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
export interface Share {
|
||||
share(options: ShareOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export const Share: Share;
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* Index file for all type declarations.
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
export * from "./interfaces";
|
||||
|
||||
import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces";
|
||||
|
||||
export interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
/**
|
||||
* @file interfaces.ts
|
||||
* @description Core type declarations for the TimeSafari application
|
||||
*
|
||||
* This module defines the core interfaces and types used throughout the application.
|
||||
* It serves as the central location for type definitions that are shared across
|
||||
* multiple components and services.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. DID (Decentralized Identifier) Types:
|
||||
* - IIdentifier: Core DID structure
|
||||
* - IKey: Cryptographic key information
|
||||
* - IService: Service endpoints and capabilities
|
||||
*
|
||||
* 2. Verifiable Credential Types:
|
||||
* - GenericCredWrapper: Base wrapper for all credentials
|
||||
* - GiveVerifiableCredential: Gift-related credentials
|
||||
* - OfferVerifiableCredential: Offer-related credentials
|
||||
* - RegisterVerifiableCredential: Registration credentials
|
||||
*
|
||||
* 3. Service Types:
|
||||
* - EndorserService: Claims and endorsements
|
||||
* - PushNotificationService: Web push notifications
|
||||
* - ProfileService: User profiles
|
||||
* - BackupService: Data backup
|
||||
*
|
||||
* @see src/interfaces/identifier.ts
|
||||
* @see src/interfaces/claims.ts
|
||||
* @see src/interfaces/limits.ts
|
||||
*/
|
||||
|
||||
import { GiveVerifiableCredential } from "../interfaces";
|
||||
|
||||
/**
|
||||
* Interface for a Decentralized Identifier (DID)
|
||||
*
|
||||
* This interface defines the structure of a DID, which is a unique identifier
|
||||
* that can be used to look up a DID document containing information associated
|
||||
* with the DID, such as public keys and service endpoints.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const identifier: IIdentifier = {
|
||||
* did: 'did:ethr:0x123...',
|
||||
* provider: 'ethr',
|
||||
* keys: [{
|
||||
* kid: 'keys-1',
|
||||
* kms: 'local',
|
||||
* type: 'Secp256k1',
|
||||
* publicKeyHex: '0x...',
|
||||
* meta: { derivationPath: "m/44'/60'/0'/0/0" }
|
||||
* }],
|
||||
* services: [{
|
||||
* id: 'endorser-service',
|
||||
* type: 'EndorserService',
|
||||
* serviceEndpoint: 'https://api.endorser.ch'
|
||||
* }]
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface IIdentifier {
|
||||
/**
|
||||
* The DID string in the format 'did:method:identifier'
|
||||
* @example 'did:ethr:0x123...'
|
||||
*/
|
||||
did: string;
|
||||
|
||||
/**
|
||||
* The DID method provider
|
||||
* @example 'ethr'
|
||||
*/
|
||||
provider: string;
|
||||
|
||||
/**
|
||||
* Array of cryptographic keys associated with the DID
|
||||
*/
|
||||
keys: Array<{
|
||||
/**
|
||||
* Key identifier
|
||||
* @example 'keys-1'
|
||||
*/
|
||||
kid: string;
|
||||
|
||||
/**
|
||||
* Key management system
|
||||
* @example 'local'
|
||||
*/
|
||||
kms: string;
|
||||
|
||||
/**
|
||||
* Key type
|
||||
* @example 'Secp256k1'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Public key in hexadecimal format
|
||||
* @example '0x...'
|
||||
*/
|
||||
publicKeyHex: string;
|
||||
|
||||
/**
|
||||
* Optional metadata about the key
|
||||
*/
|
||||
meta?: {
|
||||
/**
|
||||
* HD wallet derivation path
|
||||
* @example "m/44'/60'/0'/0/0"
|
||||
*/
|
||||
derivationPath?: string;
|
||||
|
||||
/**
|
||||
* Key usage or purpose
|
||||
* @example "signing", "encryption"
|
||||
*/
|
||||
usage?: string;
|
||||
|
||||
/**
|
||||
* Key creation timestamp
|
||||
*/
|
||||
createdAt?: number;
|
||||
|
||||
/**
|
||||
* Additional key metadata
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Array of service endpoints associated with the DID
|
||||
*/
|
||||
services: Array<{
|
||||
/**
|
||||
* Service identifier
|
||||
* @example 'endorser-service'
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Service type
|
||||
* @example 'EndorserService'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Service endpoint URL
|
||||
* @example 'https://api.endorser.ch'
|
||||
*/
|
||||
serviceEndpoint: string;
|
||||
|
||||
/**
|
||||
* Optional service description
|
||||
*/
|
||||
description?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Optional metadata about the identifier
|
||||
*/
|
||||
meta?: {
|
||||
/**
|
||||
* DID method-specific metadata
|
||||
* @example { network: "mainnet", chainId: 1 } for ethr
|
||||
*/
|
||||
method?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Identifier creation timestamp
|
||||
*/
|
||||
createdAt?: number;
|
||||
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updatedAt?: number;
|
||||
|
||||
/**
|
||||
* Additional identifier metadata
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a cryptographic key
|
||||
*
|
||||
* This interface defines the structure of a cryptographic key used in the
|
||||
* DID system. It includes both public and private key information, along
|
||||
* with metadata about the key's purpose and derivation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const key: IKey = {
|
||||
* id: 'did:ethr:0x123...#keys-1',
|
||||
* type: 'Secp256k1VerificationKey2018',
|
||||
* controller: 'did:ethr:0x123...',
|
||||
* ethereumAddress: '0x123...',
|
||||
* publicKeyHex: '0x...',
|
||||
* privateKeyHex: '0x...',
|
||||
* meta: {
|
||||
* derivationPath: "m/44'/60'/0'/0/0"
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface IKey {
|
||||
/**
|
||||
* Unique identifier for the key
|
||||
* @example 'did:ethr:0x123...#keys-1'
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Key type specification
|
||||
* @example 'Secp256k1VerificationKey2018'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* DID that controls this key
|
||||
* @example 'did:ethr:0x123...'
|
||||
*/
|
||||
controller: string;
|
||||
|
||||
/**
|
||||
* Associated Ethereum address
|
||||
* @example '0x123...'
|
||||
*/
|
||||
ethereumAddress: string;
|
||||
|
||||
/**
|
||||
* Public key in hexadecimal format
|
||||
* @example '0x...'
|
||||
*/
|
||||
publicKeyHex: string;
|
||||
|
||||
/**
|
||||
* Private key in hexadecimal format
|
||||
* @example '0x...'
|
||||
*/
|
||||
privateKeyHex: string;
|
||||
|
||||
/**
|
||||
* Optional metadata about the key
|
||||
*/
|
||||
meta?: {
|
||||
/**
|
||||
* HD wallet derivation path
|
||||
* @example "m/44'/60'/0'/0/0"
|
||||
*/
|
||||
derivationPath?: string;
|
||||
|
||||
/**
|
||||
* Key usage or purpose
|
||||
* @example "signing", "encryption"
|
||||
*/
|
||||
usage?: string;
|
||||
|
||||
/**
|
||||
* Key creation timestamp
|
||||
*/
|
||||
createdAt?: number;
|
||||
|
||||
/**
|
||||
* Additional key metadata
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a service endpoint
|
||||
*
|
||||
* This interface defines the structure of a service endpoint that can be
|
||||
* associated with a DID. Services provide additional functionality and
|
||||
* endpoints for DID operations.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const service: IService = {
|
||||
* id: 'endorser-service',
|
||||
* type: 'EndorserService',
|
||||
* serviceEndpoint: 'https://api.endorser.ch',
|
||||
* description: 'Service for handling claims and endorsements',
|
||||
* metadata: {
|
||||
* version: '1.0.0',
|
||||
* capabilities: ['claims', 'endorsements'],
|
||||
* config: {
|
||||
* apiServer: 'https://api.endorser.ch'
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface IService {
|
||||
/**
|
||||
* Unique identifier for the service
|
||||
* @example 'endorser-service'
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Type of service
|
||||
* @example 'EndorserService'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Service endpoint URL
|
||||
* @example 'https://api.endorser.ch'
|
||||
*/
|
||||
serviceEndpoint: string;
|
||||
|
||||
/**
|
||||
* Optional human-readable description
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Optional service metadata
|
||||
*/
|
||||
metadata?: {
|
||||
/**
|
||||
* Service version
|
||||
* @example '1.0.0'
|
||||
*/
|
||||
version?: string;
|
||||
|
||||
/**
|
||||
* Array of service capabilities
|
||||
* @example ['claims', 'endorsements']
|
||||
*/
|
||||
capabilities?: string[];
|
||||
|
||||
/**
|
||||
* Service-specific configuration
|
||||
*/
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExportProgress {
|
||||
status: "preparing" | "exporting" | "complete" | "error";
|
||||
message?: string;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
/** User's profile description */
|
||||
description: string;
|
||||
/** User's location information */
|
||||
location?: {
|
||||
/** Latitude coordinate */
|
||||
lat: number;
|
||||
/** Longitude coordinate */
|
||||
lng: number;
|
||||
};
|
||||
/** User's given name */
|
||||
givenName?: string;
|
||||
/** User's family name */
|
||||
familyName?: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
message: string;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export interface LeafletMouseEvent {
|
||||
latlng: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GiveRecordWithContactInfo {
|
||||
type?: string;
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
amountConfirmed: number;
|
||||
description: string;
|
||||
fullClaim: GiveVerifiableCredential;
|
||||
fulfillsHandleId: string;
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
handleId: string;
|
||||
issuedAt: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
providerPlanHandleId?: string;
|
||||
recipientDid: string;
|
||||
unit: string;
|
||||
giver: {
|
||||
known: boolean;
|
||||
displayName: string;
|
||||
profileImageUrl?: string;
|
||||
};
|
||||
issuer: {
|
||||
known: boolean;
|
||||
displayName: string;
|
||||
profileImageUrl?: string;
|
||||
};
|
||||
receiver: {
|
||||
known: boolean;
|
||||
displayName: string;
|
||||
profileImageUrl?: string;
|
||||
};
|
||||
providerPlanName?: string;
|
||||
recipientProjectName?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface TimeSafariError extends Error {
|
||||
/**
|
||||
* User-friendly error message
|
||||
*/
|
||||
userMessage?: string;
|
||||
|
||||
/**
|
||||
* Error code for programmatic handling
|
||||
*/
|
||||
code?: string;
|
||||
|
||||
/**
|
||||
* Additional error context
|
||||
*/
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
@@ -4,32 +4,6 @@ function safeStringify(obj: unknown) {
|
||||
const seen = new WeakSet();
|
||||
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
// Skip Vue component instance properties
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
("$el" in value || "$options" in value || "$parent" in value)
|
||||
) {
|
||||
return "[Vue Component]";
|
||||
}
|
||||
|
||||
// Handle Vue router objects
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
("fullPath" in value || "path" in value || "name" in value)
|
||||
) {
|
||||
return {
|
||||
fullPath: value.fullPath,
|
||||
path: value.path,
|
||||
name: value.name,
|
||||
params: value.params,
|
||||
query: value.query,
|
||||
hash: value.hash,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle circular references
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return "[Circular]";
|
||||
@@ -37,7 +11,6 @@ function safeStringify(obj: unknown) {
|
||||
seen.add(value);
|
||||
}
|
||||
|
||||
// Handle functions
|
||||
if (typeof value === "function") {
|
||||
return `[Function: ${value.name || "anonymous"}]`;
|
||||
}
|
||||
@@ -46,63 +19,34 @@ function safeStringify(obj: unknown) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatMessage(message: string, ...args: unknown[]): string {
|
||||
const prefix = "[TimeSafari]";
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
return `${prefix} ${message}${argsString}`;
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
log: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
if (
|
||||
process.env.NODE_ENV !== "production" ||
|
||||
process.env.VITE_PLATFORM === "capacitor"
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formattedMessage);
|
||||
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||
console.log(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
}
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
if (
|
||||
process.env.NODE_ENV !== "production" ||
|
||||
process.env.VITE_PLATFORM === "capacitor"
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(formattedMessage);
|
||||
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||
console.warn(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
}
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
// Errors will always be logged
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(formattedMessage);
|
||||
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||
console.error(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
},
|
||||
};
|
||||
|
||||
export function log(message: string, ...args: unknown[]): void {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formattedMessage);
|
||||
}
|
||||
|
||||
export function error(message: string, ...args: unknown[]): void {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(formattedMessage);
|
||||
}
|
||||
|
||||
export function warn(message: string, ...args: unknown[]): void {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(formattedMessage);
|
||||
}
|
||||
|
||||
export function info(message: string, ...args: unknown[]): void {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(formattedMessage);
|
||||
}
|
||||
|
||||
export function debug(message: string, ...args: unknown[]): void {
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(formattedMessage);
|
||||
}
|
||||
|
||||
@@ -5,17 +5,6 @@
|
||||
<router-link :to="'/claim/' + claimId">
|
||||
<canvas ref="claimCanvas" class="w-full block mx-auto"></canvas>
|
||||
</router-link>
|
||||
<div class="qr-code-container">
|
||||
<QRCodeVue
|
||||
ref="qrCodeRef"
|
||||
:value="qrCodeData"
|
||||
:size="200"
|
||||
level="H"
|
||||
render-as="svg"
|
||||
:margin="0"
|
||||
:color="{ dark: '#000000', light: '#ffffff' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -24,17 +13,13 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { nextTick } from "vue";
|
||||
import QRCodeVue from "qrcode.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import QRCode from "qrcode";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {
|
||||
QRCodeVue,
|
||||
},
|
||||
})
|
||||
@Component
|
||||
export default class ClaimCertificateView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
@@ -46,8 +31,6 @@ export default class ClaimCertificateView extends Vue {
|
||||
|
||||
serverUtil = serverUtil;
|
||||
|
||||
private qrCodeRef: InstanceType<typeof QRCodeVue> | null = null;
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
@@ -269,23 +252,19 @@ export default class ClaimCertificateView extends Vue {
|
||||
);
|
||||
|
||||
// Generate and draw QR code
|
||||
await this.generateQRCode();
|
||||
const qrCodeCanvas = document.createElement("canvas");
|
||||
await QRCode.toCanvas(
|
||||
qrCodeCanvas,
|
||||
APP_SERVER + "/claim/" + this.claimId,
|
||||
{
|
||||
width: 150,
|
||||
color: { light: "#0000" /* Transparent background */ },
|
||||
},
|
||||
);
|
||||
ctx.drawImage(qrCodeCanvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async generateQRCode() {
|
||||
if (!this.qrCodeRef) return;
|
||||
|
||||
const canvas = await this.qrCodeRef.toCanvas();
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw the QR code on the claim canvas
|
||||
const CANVAS_WIDTH = 1100;
|
||||
const CANVAS_HEIGHT = 850;
|
||||
ctx.drawImage(canvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,17 +2,6 @@
|
||||
<section id="Content">
|
||||
<div v-if="claimData">
|
||||
<canvas ref="claimCanvas"></canvas>
|
||||
<div class="qr-code-container">
|
||||
<QRCodeVue
|
||||
ref="qrCodeRef"
|
||||
:value="qrCodeData"
|
||||
:size="200"
|
||||
level="H"
|
||||
render-as="svg"
|
||||
:margin="0"
|
||||
:color="{ dark: '#000000', light: '#ffffff' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -20,19 +9,13 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { nextTick } from "vue";
|
||||
import QRCodeVue from "qrcode.vue";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import * as endorserServer from "../libs/endorserServer";
|
||||
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QRCodeVue,
|
||||
},
|
||||
})
|
||||
@Component
|
||||
export default class ClaimReportCertificateView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
@@ -40,14 +23,10 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
claimId = "";
|
||||
claimData: GenericCredWrapper<GenericVerifiableCredential> | null = null;
|
||||
claimData = null;
|
||||
|
||||
endorserServer = endorserServer;
|
||||
|
||||
private qrCodeRef: InstanceType<typeof QRCodeVue> | null = null;
|
||||
private readonly CANVAS_WIDTH = 1100;
|
||||
private readonly CANVAS_HEIGHT = 850;
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
@@ -84,12 +63,20 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async drawCanvas(claimData: GenericCredWrapper<GenericVerifiableCredential>) {
|
||||
async drawCanvas(
|
||||
claimData: endorserServer.GenericCredWrapper<endorserServer.GenericVerifiableCredential>,
|
||||
) {
|
||||
await db.open();
|
||||
const allContacts = await db.contacts.toArray();
|
||||
|
||||
const canvas = this.$refs.claimCanvas as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
const CANVAS_WIDTH = 1100;
|
||||
const CANVAS_HEIGHT = 850;
|
||||
|
||||
// size to approximate portrait of 8.5"x11"
|
||||
canvas.width = CANVAS_WIDTH;
|
||||
canvas.height = CANVAS_HEIGHT;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
// Load the background image
|
||||
@@ -97,13 +84,7 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
backgroundImage.src = "/img/background/cert-frame-2.jpg";
|
||||
backgroundImage.onload = async () => {
|
||||
// Draw the background image
|
||||
ctx.drawImage(
|
||||
backgroundImage,
|
||||
0,
|
||||
0,
|
||||
this.CANVAS_WIDTH,
|
||||
this.CANVAS_HEIGHT,
|
||||
);
|
||||
ctx.drawImage(backgroundImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Set font and styles
|
||||
ctx.fillStyle = "black";
|
||||
@@ -117,8 +98,8 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
const claimTypeWidth = ctx.measureText(claimTypeText).width;
|
||||
ctx.fillText(
|
||||
claimTypeText,
|
||||
(this.CANVAS_WIDTH - claimTypeWidth) / 2, // Center horizontally
|
||||
this.CANVAS_HEIGHT * 0.33,
|
||||
(CANVAS_WIDTH - claimTypeWidth) / 2, // Center horizontally
|
||||
CANVAS_HEIGHT * 0.33,
|
||||
);
|
||||
|
||||
if (claimData.claim.agent) {
|
||||
@@ -127,8 +108,8 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
const presentedWidth = ctx.measureText(presentedText).width;
|
||||
ctx.fillText(
|
||||
presentedText,
|
||||
(this.CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
|
||||
this.CANVAS_HEIGHT * 0.37,
|
||||
(CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
|
||||
CANVAS_HEIGHT * 0.37,
|
||||
);
|
||||
const agentText = endorserServer.didInfoForCertificate(
|
||||
claimData.claim.agent,
|
||||
@@ -138,8 +119,8 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
const agentWidth = ctx.measureText(agentText).width;
|
||||
ctx.fillText(
|
||||
agentText,
|
||||
(this.CANVAS_WIDTH - agentWidth) / 2, // Center horizontally
|
||||
this.CANVAS_HEIGHT * 0.4,
|
||||
(CANVAS_WIDTH - agentWidth) / 2, // Center horizontally
|
||||
CANVAS_HEIGHT * 0.4,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,8 +135,8 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
const descriptionWidth = ctx.measureText(descriptionLine).width;
|
||||
ctx.fillText(
|
||||
descriptionLine,
|
||||
(this.CANVAS_WIDTH - descriptionWidth) / 2,
|
||||
this.CANVAS_HEIGHT * 0.45,
|
||||
(CANVAS_WIDTH - descriptionWidth) / 2,
|
||||
CANVAS_HEIGHT * 0.45,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,43 +149,33 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
claimData.issuer,
|
||||
allContacts,
|
||||
);
|
||||
ctx.fillText(
|
||||
issuerText,
|
||||
this.CANVAS_WIDTH * 0.3,
|
||||
this.CANVAS_HEIGHT * 0.6,
|
||||
);
|
||||
ctx.fillText(issuerText, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.6);
|
||||
}
|
||||
|
||||
// Draw claim ID
|
||||
ctx.font = "14px Arial";
|
||||
ctx.fillText(
|
||||
this.claimId,
|
||||
this.CANVAS_WIDTH * 0.3,
|
||||
this.CANVAS_HEIGHT * 0.7,
|
||||
);
|
||||
ctx.fillText(this.claimId, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.7);
|
||||
ctx.fillText(
|
||||
"via EndorserSearch.com",
|
||||
this.CANVAS_WIDTH * 0.3,
|
||||
this.CANVAS_HEIGHT * 0.73,
|
||||
CANVAS_WIDTH * 0.3,
|
||||
CANVAS_HEIGHT * 0.73,
|
||||
);
|
||||
|
||||
// Generate and draw QR code
|
||||
await this.generateQRCode();
|
||||
const qrCodeCanvas = document.createElement("canvas");
|
||||
await QRCode.toCanvas(
|
||||
qrCodeCanvas,
|
||||
APP_SERVER + "/claim/" + this.claimId,
|
||||
{
|
||||
width: 150,
|
||||
color: { light: "#0000" /* Transparent background */ },
|
||||
},
|
||||
);
|
||||
ctx.drawImage(qrCodeCanvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async generateQRCode() {
|
||||
if (!this.qrCodeRef) return;
|
||||
|
||||
const canvas = await this.qrCodeRef.toCanvas();
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw the QR code on the report canvas
|
||||
ctx.drawImage(canvas, this.CANVAS_WIDTH * 0.6, this.CANVAS_HEIGHT * 0.55);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -215,18 +186,5 @@ canvas {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow">
|
||||
<img
|
||||
src="@/assets/blank-square.svg"
|
||||
src="../assets/blank-square.svg"
|
||||
width="32"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
/>
|
||||
|
||||
@@ -484,13 +484,13 @@
|
||||
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
||||
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
||||
<img
|
||||
src="@/assets/help/creative-commons-circle.svg"
|
||||
src="../assets/help/creative-commons-circle.svg"
|
||||
alt="CC circle"
|
||||
width="20"
|
||||
class="display: inline"
|
||||
/>
|
||||
<img
|
||||
src="@/assets/help/creative-commons-zero.svg"
|
||||
src="../assets/help/creative-commons-zero.svg"
|
||||
alt="CC zero"
|
||||
width="20"
|
||||
style="display: inline"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
/>
|
||||
|
||||
<h3
|
||||
class="text-sm uppercase font-semibold mb-3"
|
||||
class="text-blue-500 text-sm font-semibold mb-3"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
>
|
||||
Advanced
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
</li>
|
||||
<li @click="openGiftDialogToProject()">
|
||||
<img
|
||||
src="@/assets/blank-square.svg"
|
||||
src="../assets/blank-square.svg"
|
||||
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
@@ -361,7 +361,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-bold mt-4">Given To This Idea</h3>
|
||||
<h3 class="text-lg font-bold mt-4">Given To This Project</h3>
|
||||
|
||||
<div v-if="givesToThis.length === 0" class="text-sm">
|
||||
(None yet. If you've seen something, say something by clicking a
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -74,30 +74,13 @@ import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUt
|
||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||
// Load app homepage
|
||||
await page.goto('./');
|
||||
|
||||
// Wait for and dismiss onboarding dialog, with retry logic
|
||||
const closeOnboarding = async () => {
|
||||
const closeButton = page.getByTestId('closeOnboardingAndFinish');
|
||||
if (await closeButton.isVisible()) {
|
||||
await closeButton.click();
|
||||
await expect(closeButton).toBeHidden();
|
||||
}
|
||||
};
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
|
||||
// Initial dismissal
|
||||
await closeOnboarding();
|
||||
|
||||
// Wait for network to be idle
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check and dismiss onboarding again if it reappeared
|
||||
await closeOnboarding();
|
||||
|
||||
// Wait for initial feed items to load
|
||||
// Check that initial 10 activities have been loaded
|
||||
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible();
|
||||
|
||||
|
||||
// Scroll down a bit to trigger loading additional activities
|
||||
await page.locator('ul#listLatestActivity li:nth-child(20)').scrollIntoViewIfNeeded();
|
||||
await page.locator('ul#listLatestActivity li:nth-child(50)').scrollIntoViewIfNeeded();
|
||||
});
|
||||
|
||||
test('Check discover results', async ({ page }) => {
|
||||
@@ -121,11 +104,8 @@ test('Check no-ID messaging in account', async ({ page }) => {
|
||||
// Check 'a friend needs to register you' notice
|
||||
await expect(page.locator('#noticeBeforeAnnounce')).toBeVisible();
|
||||
|
||||
// Check that there is no ID by finding the wrapper first
|
||||
const didWrapper = page.locator('[data-testId="didWrapper"]');
|
||||
await expect(didWrapper).toBeVisible();
|
||||
const codeElement = didWrapper.locator('code[role="code"]');
|
||||
await expect(codeElement).toBeEmpty();
|
||||
// Check that there is no ID
|
||||
await expect(page.locator('#sectionIdentityDetails code.truncate')).toBeEmpty();
|
||||
});
|
||||
|
||||
test('Check ability to share contact', async ({ page }) => {
|
||||
@@ -189,14 +169,7 @@ test('Check setting name & sharing info', async ({ page }) => {
|
||||
test('Confirm test API setting (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
|
||||
// Load account view
|
||||
await page.goto('./account');
|
||||
|
||||
// Wait for and click the Advanced heading
|
||||
const advancedHeading = page.getByRole('heading', { name: 'Advanced' });
|
||||
await advancedHeading.waitFor({ state: 'visible' });
|
||||
await advancedHeading.click();
|
||||
|
||||
// Wait for the Advanced section to be fully loaded
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByRole('heading', { name: 'Advanced' }).click();
|
||||
|
||||
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
||||
const webServer = testInfo.config.webServer;
|
||||
@@ -205,12 +178,8 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
|
||||
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '='));
|
||||
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1);
|
||||
|
||||
// Find the Claim Server input field using the label's for attribute
|
||||
const serverInput = page.locator('input[type="text"]').first();
|
||||
await serverInput.waitFor({ state: 'visible' });
|
||||
|
||||
const endorserServer = endorserTermInConfig || 'https://api.endorser.ch';
|
||||
await expect(serverInput).toHaveValue(endorserServer);
|
||||
const endorserServer = endorserTermInConfig || 'https://test-api.endorser.ch';
|
||||
await expect(page.getByRole('textbox').nth(1)).toHaveValue(endorserServer);
|
||||
});
|
||||
|
||||
test('Check User 0 can register a random person', async ({ page }) => {
|
||||
|
||||
@@ -1,36 +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/*"],
|
||||
"@/types/*": ["types/*"]
|
||||
},
|
||||
"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" }]
|
||||
}
|
||||
|
||||