Compare commits

..

21 Commits

Author SHA1 Message Date
30e9b65ff0 update context with better focus for this project 2025-04-12 19:04:09 -06:00
f14e9dbb84 add file for context when working with LLMs 2025-04-12 19:03:02 -06:00
5cc42be58a fix some test scripts 2025-04-08 20:31:47 -06:00
3d1a2eeb8d adjust to app.timesafari.app in more places 2025-04-08 20:29:08 -06:00
7b0ee2e44e more ios folders to ignore (until we figure out the right way to dance with capacitor-assets) 2025-04-06 19:52:44 -06:00
ac018997e8 adjust instructions for capacitor-assets and more files 2025-04-06 19:48:45 -06:00
6f449e9c1f restore important file from previous cleanup 2025-04-06 19:15:00 -06:00
543599a6a1 remove icon files that are generated by capacitor-assets 2025-04-06 19:02:01 -06:00
634395ff38 fix instructions & app name 2025-04-05 17:23:29 -06:00
da1f08ebaa move the android secrets files in proximity to where they're used 2025-04-05 16:25:17 -06:00
4ee3ce0061 make changes that must have been done by XCode 2025-04-05 16:08:12 -06:00
654c67af72 add important ios files that aren't regenerated 2025-04-05 16:03:44 -06:00
b244f609b3 fix linting 2025-04-05 15:16:01 -06:00
9c84302c2e consolidate build & test instructions 2025-04-05 15:04:28 -06:00
ca37c30180 add instructions for the release build 2025-04-04 20:10:09 -06:00
130139e2af fix the build config to allow signing, either with a secrets file or with env vars 2025-04-04 20:01:23 -06:00
9802deb17c Merge pull request 'Adjustments to source-destination graphic' (#129) from homeview-card-design-2025-04 into master
Reviewed-on: #129
2025-04-03 21:50:30 -04:00
76c983ea3e replace with real designed icon 2025-04-03 17:55:59 -06:00
114ef440b8 add more build instructions for iOS 2025-04-03 17:52:03 -06:00
b58d510f24 make Advanced links explicit and use "project" instead of "idea" in project page 2025-04-03 17:50:49 -06:00
Jose Olarte III
770c0fa77c Adjustments to source-destination graphic 2025-03-31 21:46:50 +08:00
109 changed files with 2920 additions and 5804 deletions

View File

@@ -1 +0,0 @@
PLATFORM=electron

View File

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

View File

@@ -1 +0,0 @@
PLATFORM=web

View File

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

15
.gitignore vendored
View File

@@ -39,28 +39,17 @@ 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/
# PWA icon files generated by capacitor-assets
icons

View File

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

96
README-context.md Normal file
View File

@@ -0,0 +1,96 @@
# Time Safari Context
## Project Overview
Time Safari is an application designed to foster community building through gifts, gratitude, and collaborative projects. The app should make it extremely easy and intuitive for users of any age and capability to recognize contributions, build trust networks, and organize collective action. It is built on services that preserve privacy and data sovereignty.
The ultimate goals of Time Safari are two-fold:
1. **Connect** Make it easy, rewarding, and non-threatening for people to connect with others who have similar interests, and to initiate activities together. This helps people accomplish and learn from other individuals in less-structured environments; moreover, it helps them discover who they want to continue to support and with whom they want to maintain relationships.
2. **Reveal** Widely advertise the great support and rewards that are being given and accepted freely, especially non-monetary ones. Using visuals and text, display the kind of impact that gifts are making in the lives of others. Also show useful and engaging reports of project statistics and personal accomplishments.
## Core Approaches
Time Safari should help everyday users build meaningful connections and organize collective efforts by:
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts and contributions people give to each other and their communities.
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask for or propose help on projects and interests that matter to them.
3. **Building Trust Networks**: Enabling users to maintain their network and activity visibility. Developing reputation through verified contributions and references, which can be selectively shown to others outside the network.
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with explicitly authorized contacts, allowing private individuals including children to participate safely.
5. **Engaging Content**: Displaying people's records in compelling stories, and highlighting those projects that are lifting people's lives long-term, both in physical support and in emotional-spiritual-creative thriving.
## Technical Foundation
This application is built on a privacy-preserving claims architecture (via endorser.ch) with these key characteristics:
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private key pairs stored on their devices
- **Cryptographic Verification**: All claims and confirmations are cryptographically signed
- **User-Controlled Visibility**: Users explicitly control who can see their identifiers and data
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification and integrity
- **Native and Web App**: Works on iOS, Android, and web browsers
## User Journey
The typical progression of usage follows these stages:
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude for gifts received, building a foundation of acknowledgment.
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect with others who share similar interests.
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute proposed projects, moving from ideas to action.
## Context for LLM Development
When developing new functionality for Time Safari, consider these design principles:
1. **Accessibility First**: Features should be usable by non-technical users with minimal learning curve.
2. **Privacy by Design**: All features must respect user privacy and data sovereignty.
3. **Progressive Enhancement**: Core functionality should work across all devices, with richer experiences where supported.
4. **Voluntary Collaboration**: The system should enable but never coerce participation.
5. **Trust Building**: Features should help build verifiable trust between users.
6. **Network Effects**: Consider how features scale as more users join the platform.
7. **Low Resource Requirements**: The system should be lightweight enough to run on inexpensive devices users already own.
## Use Cases to Support
LLM development should focus on enhancing these key use cases:
1. **Community Building**: Tools that help people find others with shared interests and values.
2. **Project Coordination**: Features that make it easy to propose collaborative projects and to submit suggestions and offers to existing ones.
3. **Reputation Building**: Methods for users to showcase their contributions and reliability, in contexts where they explicitly reveal that information.
4. **Governance Experimentation**: Features that facilitate decision-making and collective governance.
## Constraints
When developing new features, be mindful of these constraints:
1. **Privacy Preservation**: User identifiers must remain private except when explicitly shared.
2. **Platform Limitations**: Features must work within the constraints of the target app platforms, while aiming to leverage the best platform technology available.
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch API capabilities.
4. **Performance on Low-End Devices**: The application should remain performant on older/simpler devices.
5. **Offline-First When Possible**: Key functionality should work offline when feasible.

View File

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

8
android/.gitignore vendored
View File

@@ -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
@@ -99,3 +102,8 @@ app/src/main/assets/public
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
# Generated Icons from capacitor-assets
app/src/main/res/drawable/*.png
app/src/main/res/drawable-*/*.png
app/src/main/res/mipmap-*/*.png

View File

@@ -1,2 +1,2 @@
#Thu Apr 03 10:21:42 UTC 2025
gradle.version=8.11.1
#Fri Mar 21 07:27:50 UTC 2025
gradle.version=8.2.1

Binary file not shown.

View File

@@ -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("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 9
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
}
}
}

View File

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

View File

@@ -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());
}
}

View File

@@ -1,5 +1,5 @@
{
"appId": "app.timesafari",
"appId": "app.timesafari.app",
"appName": "TimeSafari",
"webDir": "dist",
"bundledWebRuntime": false,

View File

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

View File

@@ -6,7 +6,7 @@
<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>
<script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script>
</head>
<body>
<noscript>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

@@ -4,9 +4,3 @@ 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-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')

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

27
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata
*.xcuserstate
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml
# User-specific Xcode files
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
App/App.xcodeproj/*.xcuserstate
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
App/App/Assets.xcassets/AppIcon.appiconset
App/App/Assets.xcassets/Splash.imageset

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Time Safari.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View 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)
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

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

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

49
ios/App/App/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?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>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/>
</dict>
</plist>

24
ios/App/Podfile Normal file
View File

@@ -0,0 +1,24 @@
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'
end
target 'App' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
assertDeploymentTarget(installer)
end

28
ios/App/Podfile.lock Normal file
View File

@@ -0,0 +1,28 @@
PODS:
- Capacitor (6.2.0):
- CapacitorCordova
- CapacitorApp (6.0.2):
- Capacitor
- CapacitorCordova (6.2.0)
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
EXTERNAL SOURCES:
Capacitor:
:path: "../../node_modules/@capacitor/ios"
CapacitorApp:
:path: "../../node_modules/@capacitor/app"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
SPEC CHECKSUMS:
Capacitor: 05d35014f4425b0740fc8776481f6a369ad071bf
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9
PODFILE CHECKSUM: 4233f5c5f414604460ff96d372542c311b0fb7a8
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,414 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* Time Safari.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Time Safari.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>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
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 = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* 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 /* Time Safari.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 */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* Time Safari */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Time Safari" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "Time Safari";
productName = App;
productReference = 504EC3041FED79650016851F /* Time Safari.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "Time Safari" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* Time Safari */,
);
};
/* 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 = 1;
DEVELOPMENT_TEAM = GM3FS5JQPH;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Time Safari";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
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 = 1;
DEVELOPMENT_TEAM = GM3FS5JQPH;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Time Safari";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
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 "Time Safari" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Time Safari" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

1455
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",
@@ -45,13 +45,10 @@
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.1",
"@capacitor/filesystem": "^6.0.3",
"@capacitor/core": "^6.2.0",
"@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3",
"@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 +98,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 +123,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 +133,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,27 +142,20 @@
"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"
},
"main": "./dist-electron/main.js",
"build": {
"appId": "app.timesafari",
"appId": "app.timesafari.app",
"productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages"

View File

@@ -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. */
// {

View File

@@ -40,10 +40,10 @@ export default defineConfig({
permissions: ["clipboard-read"],
},
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'webkit',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,6 @@
export interface GenericVerifiableCredential {
"@context"?: string;
"@type": string;
name?: string;
description?: string;
agent?: { identifier: string } | string;
[key: string]: unknown;
}

View File

@@ -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[];
}

View File

@@ -5,4 +5,3 @@ export * from "./limits";
export * from "./records";
export * from "./user";
export * from "./deepLinks";
export * from "./identifier";

View File

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

View File

@@ -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 });

View File

@@ -50,8 +50,6 @@ import {
} from "../interfaces";
import { logger } from "../utils/logger";
export type { GenericVerifiableCredential, GenericCredWrapper };
/**
* Standard context for schema.org data
* @constant {string}

View File

@@ -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");

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -1,195 +0,0 @@
/**
* @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";
/**
* Factory class for creating platform-specific service implementations
*
* 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.
*
* @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);
* ```
*/
export class PlatformServiceFactory {
/**
* Singleton instance of the factory
* @private
*/
private static instance: PlatformServiceFactory;
/**
* Current platform identifier
* @private
*/
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();
}
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;
}
}

View File

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

View File

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

View File

@@ -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,
});
}
}

View File

@@ -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");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,28 @@ 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);
// 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);
// 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);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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