Compare commits

...

37 Commits

Author SHA1 Message Date
Matthew Raymer
3c515719e0 chore: update packages after merging electron fixes 2025-03-26 11:45:54 +00:00
Matthew Raymer
5bc7e93b4b Merge branch 'electron_fix_20250317' into fix-service-worker 2025-03-26 11:00:22 +00:00
Matthew Raymer
ee6124021d fix: improve service worker registration and debugging
- Add detailed logging for service worker registration process
- Fix PWA enabling logic in vite config to properly handle web builds
- Update serve script to explicitly set production mode
- Add better error handling and registration status reporting
2025-03-26 10:21:36 +00:00
Jose Olarte III
5143c65337 Reinforce entity icon sizes 2025-03-25 20:05:17 +08:00
Jose Olarte III
09ee94d5a3 Linting 2025-03-25 19:49:24 +08:00
071792b97c on home page: fix images for all persons, remove excessive verbiage, fix project icon, allow click on image to close 2025-03-24 20:51:16 -06:00
bf2f23021f change 'fa' to 'font-awesome' 2025-03-24 19:29:01 -06:00
829870b16c add some logging to the DB (especially for iOS app feed debugging) 2025-03-24 19:26:47 -06:00
Matthew Raymer
ae25a066f2 Merge branch 'master' into electron_fix_20250317 2025-03-24 12:19:33 +00:00
Matthew Raymer
44ffeebabe chore: chang applicationId in iOS for consistency 2025-03-24 09:01:56 +00:00
Matthew Raymer
bed3bfa387 Merge branch 'homeview-refresh-2025-02'
refactor: Extract ActivityListItem component and add claim confirmation

- Move activity list item from HomeView to dedicated component
- Add claim confirmation functionality with AgreeAction schema
- Update feed data handling for confirmation status
- Improve error handling with structured logging
- Add user confirmation dialog for claim verification

The changes improve code organization by:
1. Separating activity item UI into reusable component
2. Adding proper type definitions for activity records
3. Implementing structured claim confirmation flow
4. Adding user feedback for confirmation actions
5. Improving error handling with logger utility

Technical details:
- Added ActivityListItem.vue component
- Added confirmClaim method with schema.org AgreeAction
- Updated feed refresh after confirmation
- Added proper TypeScript interfaces
- Improved notification handling
2025-03-24 08:52:29 +00:00
b1056fc8dd add icon asset and new capacitor step, and change "test-all" to "test:all" 2025-03-22 16:34:41 -06:00
189bfabcf8 add LogView for those cases where the log download doesn't work 2025-03-22 15:15:17 -06:00
Matthew Raymer
aed1a9fea8 fix: Update Android package name and improve test reliability
- Change package name from app.timesafari.app to app.timesafari
- Fix ADB connection issues with server restart and retries
- Add interactive test flow with user prompts between tests
- Generate test data locally instead of external script
- Add proper cleanup of readline interface
- Fix URL scheme registration in Android manifest

Technical changes:
1. Remove duplicate SDK suppression entries
2. Update Gradle and build configurations
3. Add retry logic for flaky ADB connections
4. Add proper error handling for test data generation
5. Update all package references consistently

The changes improve Android testing by:
1. Making package naming consistent
2. Adding reliability to ADB connections
3. Adding user control over test flow
4. Providing better test progress visibility
5. Improving error handling and logging
2025-03-21 08:31:55 +00:00
Jose Olarte III
f71c76fcd3 Fix: removed links and elements
- Removed links from icons and giver name
- Removed confirm button
- Lint fixes
2025-03-21 15:52:45 +08:00
Matthew Raymer
d024db2258 feature: move amount and wire it up 2025-03-21 07:03:31 +00:00
Matthew Raymer
c760385dcf fix: improve DeepLinkErrorView code quality
- Fix TypeScript property error by using validRoutes constant
- Replace console.log with logger utility
- Fix string quote consistency
- Improve v-for loop variable naming
- Add proper type safety for route parameters

Technical Changes:
- Use VALID_DEEP_LINK_ROUTES constant for route list
- Add logger import and replace console.log calls
- Standardize string quotes to double quotes
- Rename v-for variable to avoid shadowing
- Add proper type assertions for route params

This improves code quality and type safety in the
DeepLinkErrorView component while maintaining consistent
coding standards.
2025-03-20 12:04:27 +00:00
Matthew Raymer
8be8de5f1f feat(ios-testing): Enhance deeplink testing and error handling
- Improve test data generation and validation
  - Add detailed logging of generated test data
  - Implement robust validation of required fields
  - Use ts-node script for test data generation
  - Add fallback data generation with validation

- Enhance deeplink testing UX
  - Add interactive prompts between tests
  - Display detailed test progress and next steps
  - Improve error handling and test skip logic
  - Add comprehensive logging throughout test execution

- Improve DeepLinkErrorView
  - Add detailed error information display
  - Show debug information for parameters and queries
  - Enhance UI with better styling and layout
  - Add safe area spacing for iOS

- Refactor deeplink handling
  - Standardize route definitions
  - Improve parameter validation
  - Add better error logging
2025-03-20 04:34:47 -07:00
Jose Olarte III
b40604f8a6 Amount above arrow
- Needs wiring up
2025-03-19 20:47:10 +08:00
Matthew Raymer
2660b91995 wip: Improve deep link validation and error handling
- Add comprehensive route validation with zod schema
- Create type-safe DeepLinkRoute enum for all valid routes
- Add structured error handling for invalid routes
- Redirect to error page with detailed feedback
- Add better timeout handling in deeplink tests

The changes improve robustness by:
1. Validating route paths before navigation
2. Providing detailed error messages for invalid links
3. Redirecting users to dedicated error pages
4. Adding parameter validation with specific feedback
5. Improving type safety across deeplink handling
2025-03-18 09:19:35 +00:00
Matthew Raymer
474999dc9c feat(test): Comprehensive iOS test script overhaul with context-aware deeplink testing
* Add complete iOS platform cleanup and reset on each test run
* Implement interactive deeplink testing with keyboard controls between tests
* Add context awareness to verify app is running and in foreground
* Improve error handling and diagnostic messaging throughout
* Auto-register URL schemes and verify app state for reliable testing
Include prerequisites check for Xcode, CocoaPods and simulator availability
* Include prerequisites check for Xcode, CocoaPods and simulator availability
2025-03-18 00:34:17 -07:00
e825950e6e remove old script 2025-03-17 20:23:04 -06:00
a73d0a85e2 change icons to font-awesome 2025-03-17 20:22:35 -06:00
fc01e81af7 remove file that should not be committed 2025-03-17 20:21:13 -06:00
Jose Olarte III
436f40813c Source-destination compacted
- Narrower max-width
- Element sizes adjusted
- Switched to a more controllable unit for widths and heights
2025-03-17 21:03:58 +08:00
Jose Olarte III
77b296b606 Identicon responsive size fix + lint-fix 2025-03-17 17:59:59 +08:00
Matthew Raymer
4230deab1d fix: Improve Electron application stability and asset handling
- Completely rewrite main.js for reliable asset loading
- Update preload.js with proper security context isolation
- Fix file:// protocol handling for application resources
- Add proper error logging and reporting in Electron context
- Disable service workers in Electron environment
- Fix path resolution for assets in packaged application
- Add prerequisite checking for Electron builds
- Update electron-builder configuration

The changes resolve persistent issues with:
1. Missing assets in packaged application
2. Incorrect path resolution in production builds
3. Service worker conflicts in desktop environment
4. Security context handling in preload script
5. Electron build process reliability
2025-03-17 07:18:06 +00:00
Matthew Raymer
683e85f5be Merge branch 'deep_linking' 2025-03-17 03:01:53 +00:00
e3ac5fe9fe fix references to partner API server 2025-03-13 19:14:59 -06:00
Matthew Raymer
8f7d794962 fix: improve image handling and icon support
- Fix image load event handler signature
- Add alt text for accessibility
- Add building icon to FontAwesome library

Technical Changes:
- Update cacheImage event to pass only image URL
- Add proper alt text for activity images
- Add faBuilding icon to FontAwesome library

This improves image handling and accessibility while adding
needed icon support for the activity feed interface.
2025-03-07 12:58:14 +00:00
Matthew Raymer
fa7d6317b9 feat: add claim confirmation functionality to activity feed
- Add confirm button functionality to ActivityListItem
- Implement confirmation logic in HomeView
- Add proper button state handling and validation

Technical Changes:
- Add canConfirm computed property to validate confirmation ability
- Add handleConfirmClick method with proper error handling
- Pass required props (isRegistered, activeDid, confirmerIdList)
- Add confirmation dialog with user verification
- Implement claim submission with proper cleanup
- Add visual feedback for button states
- Update feed after successful confirmation

UI/UX Improvements:
- Add disabled state styling for confirm button
- Show proper error messages for invalid confirmation attempts
- Add loading and success notifications
- Improve button accessibility with proper states

Bug Fixes:
- Make apiServer optional in settings type
- Fix settings update during registration
- Add proper type checking for claim confirmation

This adds the ability to confirm claims directly from the
activity feed with proper validation, error handling, and
user feedback. The confirmation flow matches the existing
claim view confirmation functionality.
2025-03-07 10:22:53 +00:00
Jose Olarte III
4a75cdf20e Homeview changes
- Moved activity image further up the frame
- Added placeholder icon for projects
- Minor fixes
2025-03-04 20:38:14 +08:00
Jose Olarte III
79fdb9e570 Homeview design adjustments
- Added markup for poster info (needs wiring)
- Repositioned giver and receiver type icons to beside their labels
- Added "Confirm" button to bottom right of item card (timestamp repositioned to poster info as a result)
- Spacing tweaks
2025-03-03 19:34:09 +08:00
Jose Olarte III
aa09827317 Type fixes 2025-02-28 21:00:03 +08:00
Matthew Raymer
cc1780bd01 refactor: extract ActivityListItem into separate component
- Move activity list item markup from HomeView to new component
- Improve code organization and reusability
- Pass required props for claim handling and image viewing
- Maintain existing functionality while reducing component complexity
- Clean up unused commented code in HomeView

This refactor improves code maintainability by extracting the activity
feed item logic into its own component.
2025-02-28 09:34:59 +00:00
Jose Olarte III
e5d9c25ad4 Comments 2025-02-27 21:21:01 +08:00
Jose Olarte III
ef8c2e6093 In-progress: homeview design refresh
I had to comment out line 544 because it was causing errors (and seemed redundant?)
2025-02-27 21:12:29 +08:00
70 changed files with 5865 additions and 2004 deletions

View File

@@ -4,6 +4,8 @@ This guide explains how to build TimeSafari for different platforms.
## Prerequisites ## Prerequisites
For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended) - Node.js (LTS version recommended)
- npm (comes with Node.js) - npm (comes with Node.js)
- Git - Git
@@ -11,6 +13,17 @@ This guide explains how to build TimeSafari for different platforms.
- For Android builds: Android Studio with SDK installed - For Android builds: Android Studio with SDK installed
- For desktop builds: Additional build tools based on your OS - For desktop builds: Additional build tools based on your OS
## Forks
If you have forked this to make your own app, you'll want to customize the iOS & Android files. You can either edit existing ones, or you can remove the `ios` and `android` directories and regenerate them before the `npx cap sync` step in each setup.
```bash
npx cap add android
npx cap add ios
```
You'll also want to edit the deep link configuration.
## Initial Setup ## Initial Setup
1. Clone the repository: 1. Clone the repository:
@@ -114,6 +127,13 @@ Prerequisites: macOS with Xcode installed
npx cap sync ios npx cap sync ios
``` ```
3. Copy the assets:
```bash
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
npx capacitor-assets generate --ios
```
3. Open the project in Xcode: 3. Open the project in Xcode:
```bash ```bash
@@ -122,16 +142,6 @@ Prerequisites: macOS with Xcode installed
4. Use Xcode to build and run on simulator or device. 4. Use Xcode to build and run on simulator or device.
If you have forked this to make your own app, you'll want to customize the ios files:
```bash
rm -rf ios
npx cap add ios
```
... and then repeat the steps above.
### Android Build ### Android Build
Prerequisites: Android Studio with SDK installed Prerequisites: Android Studio with SDK installed
@@ -150,22 +160,19 @@ Prerequisites: Android Studio with SDK installed
npx cap sync android npx cap sync android
``` ```
3. Open the project in Android Studio: 3. Copy the assets
```bash
npx capacitor-assets generate --android
```
4. Open the project in Android Studio:
```bash ```bash
npx cap open android npx cap open android
``` ```
3. Use Android Studio to build and run on emulator or device. 5. Use Android Studio to build and run on emulator or device.
If you have forked this to make your own app, you'll want to customize the android files:
```bash
rm -rf android
npx cap add android
```
... and then: repeat the steps above, and look below for the deep link configuration.
## Building Android from the console ## Building Android from the console
@@ -177,6 +184,13 @@ If you have forked this to make your own app, you'll want to customize the andro
npx cap run android npx cap run android
``` ```
... or, to create the `aab` file, `bundle` instead of `build`:
```bash
./gradlew bundle -Dlint.baselines.continue=true
```
## Configuring Android for deep links ## Configuring Android for deep links
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file: You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
@@ -329,10 +343,10 @@ The packaged application will be in `dist/TimeSafari`
## Testing ## Testing
Run local tests: Run all tests (requires XCode and Android Studio/device):
```bash ```bash
npm run test-local npm run test:all
``` ```
See [TESTING.md](test-playwright/TESTING.md) for more details. See [TESTING.md](test-playwright/TESTING.md) for more details.
@@ -422,3 +436,19 @@ mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist
- For iOS: Xcode command line tools must be installed - For iOS: Xcode command line tools must be installed
- For Android: Correct SDK version must be installed - For Android: Correct SDK version must be installed
- Check Capacitor configuration in capacitor.config.ts - Check Capacitor configuration in capacitor.config.ts
# List all installed packages
adb shell pm list packages | grep timesafari
# Force stop the app (if it's running)
adb shell am force-stop app.timesafari
# Clear app data (if you don't want to fully uninstall)
adb shell pm clear app.timesafari
# Uninstall for all users
adb shell pm uninstall -k --user 0 app.timesafari
# Check if app is installed
adb shell pm path app.timesafari

View File

@@ -1,2 +1,2 @@
#Tue Mar 11 10:01:05 UTC 2025 #Fri Mar 21 07:27:50 UTC 2025
gradle.version=8.10.2 gradle.version=8.2.1

Binary file not shown.

View File

@@ -1,3 +0,0 @@
source "https://rubygems.org"
gem "fastlane"

View File

@@ -1,3 +1,2 @@
/build/* /build/*
!/build/.npmkeep !/build/.npmkeep
src/main/assets/public/assets/

View File

@@ -1,7 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
namespace "app.timesafari" namespace 'app.timesafari'
compileSdk rootProject.ext.compileSdkVersion compileSdk rootProject.ext.compileSdkVersion
defaultConfig { defaultConfig {
applicationId "app.timesafari" applicationId "app.timesafari"
@@ -22,17 +22,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
lintOptions {
disable 'UnsanitizedFilenameFromContentProvider'
abortOnError false
baseline file("lint-baseline.xml")
// Ignore Capacitor module issues
ignore 'DefaultLocale'
ignore 'UnsanitizedFilenameFromContentProvider'
ignore 'LintBaseline'
ignore 'LintBaselineFixed'
}
} }
repositories { repositories {

View File

@@ -1,398 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.1.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.1.0)" variant="all" version="8.1.0">
<issue
id="UnknownIssueId"
message="Unknown issue id &quot;UnsanitizedFilenameFromContentProvider&quot;"
errorLine1=" disable &apos;UnsanitizedFilenameFromContentProvider&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="26"
column="18"/>
</issue>
<issue
id="UnknownIssueId"
message="Unknown issue id &quot;UnsanitizedFilenameFromContentProvider&quot;"
errorLine1=" disable &apos;UnsanitizedFilenameFromContentProvider&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="26"
column="18"/>
</issue>
<issue
id="UnknownIssueId"
message="Unknown issue id &quot;LintBaselineFixed&quot;"
errorLine1=" ignore &apos;LintBaselineFixed&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="34"
column="17"/>
</issue>
<issue
id="UnknownIssueId"
message="Unknown issue id &quot;LintBaselineFixed&quot;"
errorLine1=" ignore &apos;LintBaselineFixed&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="34"
column="17"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
errorLine1=" String msg = String.format("
errorLine2=" ^">
<location
file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java"
line="467"
column="26"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" return mask.toUpperCase().equals(string.toUpperCase());"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/com/getcapacitor/util/HostMask.java"
line="110"
column="29"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" return mask.toUpperCase().equals(string.toUpperCase());"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/com/getcapacitor/util/HostMask.java"
line="110"
column="57"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" if (header.getKey().equalsIgnoreCase(&quot;Accept&quot;) &amp;&amp; header.getValue().toLowerCase().contains(&quot;text/html&quot;)) {"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/com/getcapacitor/WebViewLocalServer.java"
line="474"
column="93"/>
</issue>
<issue
id="SimpleDateFormat"
message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."
errorLine1=" String timeStamp = new SimpleDateFormat(&quot;yyyyMMdd_HHmmss&quot;).format(new Date());"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java"
line="504"
column="28"/>
</issue>
<issue
id="SimpleDateFormat"
message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."
errorLine1=" DateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd&apos;T&apos;HH:mm&apos;Z&apos;&quot;);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/getcapacitor/PluginResult.java"
line="44"
column="25"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `usesCleartextTraffic` is only used in API level 23 and higher (current min is 22)"
errorLine1="&lt;application android:usesCleartextTraffic=&quot;true&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="4"
column="15"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `autoVerify` is only used in API level 23 and higher (current min is 22)"
errorLine1=" &lt;intent-filter android:autoVerify=&quot;true&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="25"
column="28"/>
</issue>
<issue
id="ManifestOrder"
message="`&lt;uses-permission>` tag appears after `&lt;application>` tag"
errorLine1=" &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="47"
column="6"/>
</issue>
<issue
id="AndroidGradlePluginVersion"
message="A newer version of com.android.tools.build:gradle than 8.2.1 is available: 8.9.0. (There is also a newer version of 8.2.𝑥 available, if upgrading to 8.9.0 is difficult: 8.2.2)"
errorLine1=" classpath &apos;com.android.tools.build:gradle:8.2.1&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="12"
column="9"/>
</issue>
<issue
id="AndroidGradlePluginVersion"
message="A newer version of com.android.tools.build:gradle than 8.2.1 is available: 8.9.0. (There is also a newer version of 8.2.𝑥 available, if upgrading to 8.9.0 is difficult: 8.2.2)"
errorLine1=" classpath &apos;com.android.tools.build:gradle:8.2.1&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="18"
column="9"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.appcompat:appcompat than 1.6.1 is available: 1.7.0"
errorLine1=" implementation &quot;androidx.appcompat:appcompat:$androidxAppCompatVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="46"
column="20"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.appcompat:appcompat than 1.6.1 is available: 1.7.0"
errorLine1=" implementation &quot;androidx.appcompat:appcompat:$androidxAppCompatVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="46"
column="20"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.coordinatorlayout:coordinatorlayout than 1.2.0 is available: 1.3.0"
errorLine1=" implementation &quot;androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="47"
column="20"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.test.ext:junit than 1.1.5 is available: 1.2.1"
errorLine1=" androidTestImplementation &quot;androidx.test.ext:junit:$androidxJunitVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="51"
column="31"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.test.espresso:espresso-core than 3.5.1 is available: 3.6.1"
errorLine1=" androidTestImplementation &quot;androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="52"
column="31"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.appcompat:appcompat than 1.6.1 is available: 1.7.0"
errorLine1=" implementation &quot;androidx.appcompat:appcompat:$androidxAppCompatVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="75"
column="20"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.test.ext:junit than 1.1.5 is available: 1.2.1"
errorLine1=" androidTestImplementation &quot;androidx.test.ext:junit:$androidxJunitVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="77"
column="31"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.test.espresso:espresso-core than 3.5.1 is available: 3.6.1"
errorLine1=" androidTestImplementation &quot;androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="78"
column="31"/>
</issue>
<issue
id="Recycle"
message="This `TypedArray` should be recycled after use with `#recycle()`"
errorLine1=" TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/getcapacitor/BridgeFragment.java"
line="99"
column="32"/>
</issue>
<issue
id="Overdraw"
message="Possible overdraw: Root element paints background `#F0FF1414` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
errorLine1=" android:background=&quot;#F0FF1414&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_bridge.xml"
line="5"
column="5"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.layout.activity_main` appears to be unused"
errorLine1="&lt;androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/layout/activity_main.xml"
line="2"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.xml.config` appears to be unused"
errorLine1="&lt;widget version=&quot;1.0.0&quot; xmlns=&quot;http://www.w3.org/ns/widgets&quot; xmlns:cdv=&quot;http://cordova.apache.org/ns/1.0&quot;>"
errorLine2="^">
<location
file="src/main/res/xml/config.xml"
line="2"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_launcher_background` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_launcher_background.xml"
line="2"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_launcher_foreground` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable-v24/ic_launcher_foreground.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.package_name` appears to be unused"
errorLine1=" &lt;string name=&quot;package_name&quot;>app.timesafari.app&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="5"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.custom_url_scheme` appears to be unused"
errorLine1=" &lt;string name=&quot;custom_url_scheme&quot;>app.timesafari.app&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="6"
column="13"/>
</issue>
<issue
id="MonochromeLauncherIcon"
message="The application adaptive icon is missing a monochrome tag"
errorLine1="&lt;adaptive-icon xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;>"
errorLine2="^">
<location
file="src/main/res/mipmap-anydpi-v26/ic_launcher.xml"
line="2"
column="1"/>
</issue>
<issue
id="MonochromeLauncherIcon"
message="The application adaptive roundIcon is missing a monochrome tag"
errorLine1="&lt;adaptive-icon xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;>"
errorLine2="^">
<location
file="src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml"
line="2"
column="1"/>
</issue>
<issue
id="IconDipSize"
message="The image `splash.png` varies significantly in its density-independent (dip) size across the various density versions: drawable-land-hdpi/splash.png: 533x320 dp (800x480 px), drawable-land-mdpi/splash.png: 480x320 dp (480x320 px), drawable-land-xhdpi/splash.png: 640x360 dp (1280x720 px), drawable-land-xxhdpi/splash.png: 533x320 dp (1600x960 px), drawable-land-xxxhdpi/splash.png: 480x320 dp (1920x1280 px)">
<location
file="src/main/res/drawable-land-mdpi/splash.png"/>
<location
file="src/main/res/drawable-land-xxxhdpi/splash.png"/>
<location
file="src/main/res/drawable-land-hdpi/splash.png"/>
<location
file="src/main/res/drawable-land-xxhdpi/splash.png"/>
<location
file="src/main/res/drawable-land-xhdpi/splash.png"/>
</issue>
<issue
id="IconDuplicatesConfig"
message="The `splash.png` icon has identical contents in the following configuration folders: drawable-land-mdpi, drawable">
<location
file="src/main/res/drawable/splash.png"/>
<location
file="src/main/res/drawable-land-mdpi/splash.png"/>
</issue>
<issue
id="IconLocation"
message="Found bitmap drawable `res/drawable/splash.png` in densityless folder">
<location
file="src/main/res/drawable/splash.png"/>
</issue>
</issues>

View File

@@ -1,20 +1,26 @@
package app.timesafari.app; package com.getcapacitor.myapp;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest { public class ExampleInstrumentedTest {
@Test @Test
public void useAppContext() { public void useAppContext() throws Exception {
// Context of the app under test. // Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("app.timesafari", appContext.getPackageName()); assertEquals("app.timesafari", appContext.getPackageName());
} }
} }

View File

@@ -10,19 +10,19 @@
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:exported="true"
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask" android:launchMode="singleTask"
android:exported="true"> android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />

View File

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

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<title>TimeSafari</title> <title>TimeSafari</title>
<script type="module" crossorigin src="/assets/index-CI0bMoT0.js"></script> <script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

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

View File

@@ -1,4 +1,4 @@
package app.timesafari.app; package timesafari.app;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;

View File

@@ -2,6 +2,6 @@
<resources> <resources>
<string name="app_name">TimeSafari</string> <string name="app_name">TimeSafari</string>
<string name="title_activity_main">TimeSafari</string> <string name="title_activity_main">TimeSafari</string>
<string name="package_name">app.timesafari.app</string> <string name="package_name">timesafari.app</string>
<string name="custom_url_scheme">app.timesafari.app</string> <string name="custom_url_scheme">timesafari.app</string>
</resources> </resources>

View File

@@ -7,9 +7,8 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.1.0' classpath 'com.android.tools.build:gradle:8.2.1'
classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.gms:google-services:4.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@@ -28,10 +27,3 @@ allprojects {
task clean(type: Delete) { task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
configurations.all {
resolutionStrategy {
force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
force 'org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0'
}
}

View File

@@ -1,7 +0,0 @@
android {
lintOptions {
disable 'UnsanitizedFilenameFromContentProvider'
abortOnError false
baseline file("lint-baseline.xml")
}
}

View File

@@ -1,2 +0,0 @@
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("app.timesafari.app") # e.g. com.krausefx.app

View File

@@ -1,38 +0,0 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Build and deploy Android app"
lane :beta do
gradle(
task: "clean assembleRelease"
)
upload_to_play_store(
track: 'beta',
aab: '../app/build/outputs/bundle/release/app-release.aab'
)
end
lane :release do
gradle(
task: "clean assembleRelease"
)
upload_to_play_store(
aab: '../app/build/outputs/bundle/release/app-release.aab'
)
end
end

View File

@@ -1,40 +0,0 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android beta
```sh
[bundle exec] fastlane android beta
```
Build and deploy Android app
### android release
```sh
[bundle exec] fastlane android release
```
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@@ -21,5 +21,3 @@ org.gradle.jvmargs=-Xmx1536m
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
android.suppressUnsupportedCompileSdk=34 android.suppressUnsupportedCompileSdk=34
android.suppressUnsupportedCompileSdk=34
android.suppressUnsupportedCompileSdk=34

View File

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

View File

@@ -1,8 +0,0 @@
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Sun Mar 09 06:14:41 UTC 2025
sdk.dir=/opt/android-sdk

BIN
assets/icon-only.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -1,7 +1,7 @@
import type { CapacitorConfig } from '@capacitor/cli'; import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = { const config: CapacitorConfig = {
appId: 'app.timesafari.app', appId: 'app.timesafari',
appName: 'TimeSafari', appName: 'TimeSafari',
webDir: 'dist', webDir: 'dist',
bundledWebRuntime: false, bundledWebRuntime: false,

36
electron-builder.json Normal file
View File

@@ -0,0 +1,36 @@
{
"appId": "app.timesafari.app",
"productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages",
"buildResources": "build"
},
"files": [
"dist-electron/**/*",
"node_modules/**/*",
"package.json",
"src/electron/electron-logger.js"
],
"extraResources": [
{
"from": "src/utils",
"to": "utils",
"filter": ["**/*"]
}
],
"extraMetadata": {
"main": "src/electron/main.js"
},
"linux": {
"target": ["AppImage"],
"category": "Utility",
"maintainer": "TimeSafari Team"
},
"mac": {
"target": ["dmg"],
"category": "public.app-category.productivity"
},
"win": {
"target": ["nsis"]
}
}

3839
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,12 @@
}, },
"scripts": { "scripts": {
"dev": "vite --config vite.config.dev.mts", "dev": "vite --config vite.config.dev.mts",
"serve": "vite preview", "serve": "NODE_ENV=production vite preview --mode production --host",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts", "build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src", "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src", "lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js", "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
"test-local": "npx playwright test -c playwright.config-local.ts --trace on", "test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
"test-all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
"test:prerequisites": "node scripts/check-prerequisites.js", "test:prerequisites": "node scripts/check-prerequisites.js",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on", "test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "npm run build:capacitor && npm run test:android && npm run test:ios", "test:mobile": "npm run build:capacitor && npm run test:android && npm run test:ios",
@@ -23,14 +22,14 @@
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)", "check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
"clean:electron": "rimraf dist-electron", "clean:electron": "rimraf dist-electron",
"build:pywebview": "vite build --config vite.config.pywebview.mts", "build:pywebview": "vite build --config vite.config.pywebview.mts",
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js", "build:electron": "npm run check:electron && npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
"build:capacitor": "vite build --config vite.config.capacitor.mts", "build:capacitor": "vite build --config vite.config.capacitor.mts",
"build:web": "vite build --config vite.config.web.mts", "build:web": "vite build --config vite.config.web.mts",
"electron:dev": "npm run build && electron dist-electron", "electron:dev": "concurrently \"vite --config vite.config.electron.mts\" \"electron .\"",
"electron:start": "electron dist-electron", "electron:start": "electron dist-electron",
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage", "electron:build-linux": "npm run check:electron && npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb", "electron:build-linux-deb": "npm run check:electron && npm run build:electron && electron-builder --linux deb",
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage", "electron:build-linux-prod": "NODE_ENV=production npm run check:electron &&npm run build:electron && electron-builder --linux AppImage",
"build:electron-prod": "NODE_ENV=production npm run build:electron", "build:electron-prod": "NODE_ENV=production npm run build:electron",
"pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py", "pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
"pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py", "pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
@@ -40,7 +39,10 @@
"fastlane:ios:beta": "cd ios && fastlane beta", "fastlane:ios:beta": "cd ios && fastlane beta",
"fastlane:ios:release": "cd ios && fastlane release", "fastlane:ios:release": "cd ios && fastlane release",
"fastlane:android:beta": "cd android && fastlane beta", "fastlane:android:beta": "cd android && fastlane beta",
"fastlane:android:release": "cd android && fastlane release" "fastlane:android:release": "cd android && fastlane release",
"check:electron": "node scripts/check-electron-prerequisites.js",
"electron:build": "npm run check:electron && vite build --config vite.config.electron.mts && node scripts/fix-electron-paths.js && electron-builder",
"postinstall": "electron-builder install-app-deps"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
@@ -118,6 +120,7 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.45.2",
"@types/dom-webcodecs": "^0.1.7", "@types/dom-webcodecs": "^0.1.7",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
@@ -157,28 +160,31 @@
"build": { "build": {
"appId": "app.timesafari", "appId": "app.timesafari",
"productName": "TimeSafari", "productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages"
},
"files": [ "files": [
"dist-electron/**/*", "dist-electron/**/*",
"src/electron/**/*", "!dist-electron/node_modules/**/*"
"main.js"
], ],
"extraResources": [ "directories": {
{ "output": "dist-electron-packages",
"from": "dist-electron", "buildResources": "build-resources"
"to": "." },
} "extraResources": [],
"asar": true,
"asarUnpack": [
"dist-electron/www/assets/**/*"
], ],
"linux": { "linux": {
"target": [ "target": ["AppImage"],
"AppImage", "category": "Utility",
"deb" "executableName": "TimeSafari"
],
"category": "Office",
"icon": "build/icon.png"
}, },
"asar": true "mac": {
"category": "public.app-category.productivity"
},
"win": {
"target": ["nsis"]
},
"artifactName": "TimeSafari-${version}-${arch}.${ext}",
"publish": null
} }
} }

View File

@@ -88,6 +88,12 @@ async function main() {
throw new Error('package.json not found in build directory'); throw new Error('package.json not found in build directory');
} }
// Copy the electron-logger.js file
const loggerSrc = path.join(__dirname, '../src/electron/electron-logger.js');
const loggerDest = path.join(distElectronDir, 'electron-logger.js');
fs.copyFileSync(loggerSrc, loggerDest);
console.log(`Copying src/electron/electron-logger.js to ${loggerDest}`);
console.log('Build completed successfully!'); console.log('Build completed successfully!');
} catch (error) { } catch (error) {
console.error('Build failed:', error); console.error('Build failed:', error);

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env node
/**
* @file check-electron-prerequisites.js
* @description Verifies and installs required dependencies for Electron builds
*
* This script checks if Python's distutils module is available, which is required
* by node-gyp when compiling native Node.js modules during Electron packaging.
* Without distutils, builds will fail with "ModuleNotFoundError: No module named 'distutils'".
*
* The script performs the following actions:
* 1. Checks if Python's distutils module is available
* 2. If missing, offers to install setuptools package which provides distutils
* 3. Attempts installation through pip or pip3
* 4. Provides manual installation instructions if automated installation fails
*
* Usage:
* - Direct execution: node scripts/check-electron-prerequisites.js
* - As npm script: npm run check:electron
* - Before builds: npm run check:electron && electron-builder
*
* Exit codes:
* - 0: All prerequisites are met or were successfully installed
* - 1: Prerequisites are missing and weren't installed
*
* @author [YOUR_NAME]
* @version 1.0.0
* @license MIT
*/
const { execSync } = require('child_process');
const readline = require('readline');
const chalk = require('chalk'); // You might need to add this to your dependencies
console.log(chalk.blue('🔍 Checking Electron build prerequisites...'));
/**
* Checks if Python's distutils module is available
*
* This function attempts to import the distutils module in Python.
* If successful, it means node-gyp will be able to compile native modules.
* If unsuccessful, the Electron build will likely fail when compiling native dependencies.
*
* @returns {boolean} True if distutils is available, false otherwise
*
* @example
* if (checkDistutils()) {
* console.log('Ready to build Electron app');
* }
*/
function checkDistutils() {
try {
// Attempt to import distutils using Python
// We use stdio: 'ignore' to suppress any Python output
execSync('python -c "import distutils"', { stdio: 'ignore' });
console.log(chalk.green('✅ Python distutils is available'));
return true;
} catch (e) {
// This error occurs if either Python is not found or if distutils is missing
console.log(chalk.red('❌ Python distutils is missing'));
return false;
}
}
/**
* Installs the setuptools package which provides distutils
*
* This function attempts to install setuptools using pip or pip3.
* Setuptools is a package that provides the distutils module needed by node-gyp.
* In Python 3.12+, distutils was moved out of the standard library into setuptools.
*
* The function tries multiple installation methods:
* 1. First attempts with pip
* 2. If that fails, tries with pip3
* 3. If both fail, provides instructions for manual installation
*
* @returns {Promise<boolean>} True if installation succeeded, false otherwise
*
* @example
* const success = await installSetuptools();
* if (success) {
* console.log('Ready to proceed with build');
* } else {
* console.log('Please fix prerequisites manually');
* }
*/
async function installSetuptools() {
console.log(chalk.yellow('📦 Attempting to install setuptools...'));
try {
// First try with pip, commonly used on all platforms
execSync('pip install setuptools', { stdio: 'inherit' });
console.log(chalk.green('✅ Successfully installed setuptools'));
return true;
} catch (pipError) {
try {
// If pip fails, try with pip3 (common on Linux distributions)
console.log(chalk.yellow('⚠️ Trying with pip3...'));
execSync('pip3 install setuptools', { stdio: 'inherit' });
console.log(chalk.green('✅ Successfully installed setuptools using pip3'));
return true;
} catch (pip3Error) {
// If both methods fail, provide manual installation guidance
console.log(chalk.red('❌ Failed to install setuptools automatically'));
console.log(chalk.yellow('📝 Please install it manually with:'));
console.log(' pip install setuptools');
console.log(' or');
console.log(' sudo apt install python3-setuptools (on Debian/Ubuntu)');
console.log(' sudo pacman -S python-setuptools (on Arch Linux)');
console.log(' sudo dnf install python3-setuptools (on Fedora)');
console.log(' brew install python-setuptools (on macOS with Homebrew)');
return false;
}
}
}
/**
* Main execution function
*
* This function orchestrates the checking and installation process:
* 1. Checks if distutils is already available
* 2. If not, informs the user and prompts for installation
* 3. Based on user input, attempts to install or exits
*
* The function handles interactive user prompts and orchestrates
* the overall flow of the script.
*
* @returns {Promise<void>}
* @throws Will exit process with code 1 if prerequisites aren't met
*/
async function main() {
// First check if distutils is already available
if (checkDistutils()) {
// All prerequisites are met, exit successfully
process.exit(0);
}
// Inform the user about the missing prerequisite
console.log(chalk.yellow('⚠️ Python distutils is required for Electron builds'));
console.log(chalk.yellow('⚠️ This is needed to compile native modules during the build process'));
// Set up readline interface for user interaction
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Prompt the user for installation permission
const answer = await new Promise(resolve => {
rl.question(chalk.blue('Would you like to install setuptools now? (y/n) '), resolve);
});
// Clean up readline interface
rl.close();
if (answer.toLowerCase() === 'y') {
// User agreed to installation
const success = await installSetuptools();
if (success) {
// Installation succeeded, exit successfully
process.exit(0);
} else {
// Installation failed, exit with error
process.exit(1);
}
} else {
// User declined installation
console.log(chalk.yellow('⚠️ Build may fail without distutils'));
process.exit(1);
}
}
// Execute the main function and handle any uncaught errors
main().catch(error => {
console.error(chalk.red('Error during prerequisites check:'), error);
process.exit(1);
});

View File

@@ -0,0 +1,61 @@
/**
* Fix path resolution issues in the Electron build
*/
const fs = require('fs');
const path = require('path');
const glob = require('glob');
// Fix asset paths in HTML file
function fixHtmlPaths() {
const htmlFile = path.join(__dirname, '../dist-electron/index.html');
if (fs.existsSync(htmlFile)) {
let html = fs.readFileSync(htmlFile, 'utf8');
// Convert absolute paths to relative
html = html.replace(/src="\//g, 'src="./');
html = html.replace(/href="\//g, 'href="./');
fs.writeFileSync(htmlFile, html);
console.log('✅ Fixed paths in index.html');
}
}
// Fix asset imports in JS files
function fixJsPaths() {
const jsFiles = glob.sync('dist-electron/assets/*.js');
jsFiles.forEach(file => {
let content = fs.readFileSync(file, 'utf8');
// Replace absolute imports with relative ones
const originalContent = content;
content = content.replace(/["']\/assets\//g, '"./assets/');
if (content !== originalContent) {
fs.writeFileSync(file, content);
console.log(`✅ Fixed paths in ${path.basename(file)}`);
}
});
}
// Add base href to HTML
function addBaseHref() {
const htmlFile = path.join(__dirname, '../dist-electron/index.html');
if (fs.existsSync(htmlFile)) {
let html = fs.readFileSync(htmlFile, 'utf8');
// Add base href if not present
if (!html.includes('<base href=')) {
html = html.replace('</head>', '<base href="./">\n</head>');
fs.writeFileSync(htmlFile, html);
console.log('✅ Added base href to index.html');
}
}
}
// Run all fixes
fixHtmlPaths();
fixJsPaths();
addBaseHref();
console.log('🎉 Electron path fixes completed');

14
scripts/notarize.js Normal file
View File

@@ -0,0 +1,14 @@
// This is a placeholder notarize script that does nothing for non-macOS platforms
// Only necessary for macOS app store submissions
exports.default = async function notarizing(context) {
// Only notarize macOS builds
if (context.electronPlatformName !== 'darwin') {
console.log('Skipping notarization for non-macOS platform');
return;
}
// For macOS, we would implement actual notarization here
console.log('This is where macOS notarization would happen');
// We're just returning with no action for non-macOS builds
};

View File

@@ -30,6 +30,7 @@
* *
* @requires child_process * @requires child_process
* @requires path * @requires path
* @requires readline
* *
* @author TimeSafari Team * @author TimeSafari Team
* @license MIT * @license MIT
@@ -37,7 +38,14 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { join } = require('path'); const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs'); const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
// Format date as YYYY-MM-DD-HHMMSS // Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => { const getLogFileName = () => {
@@ -88,11 +96,57 @@ const verifyJavaInstallation = (log) => {
// Generate test data using generate_data.ts // Generate test data using generate_data.ts
const generateTestData = async (log) => { const generateTestData = async (log) => {
log('🔄 Generating test data...'); log('🔄 Generating test data...');
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
try { try {
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' }); // Generate test data
const testData = {
CONTACT1_DID: "did:ethr:0x1943754837A09684Fd6380C1D80aa53E3F20E338",
CLAIM_ID: "01JPVVX7FH0EKQWTQY9HTXZQDZ"
};
const claimDetails = {
claim_id: "01JPVVX7FH0EKQWTQY9HTXZQDZ",
issuedAt: "2025-03-21T08:07:57ZZ",
issuer: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"
};
const contacts = [
{
did: "did:ethr:0x1943754837A09684Fd6380C1D80aa53E3F20E338",
name: "Test Contact"
}
];
// Write files
log('📝 Writing test data files...');
writeFileSync('.generated/test-env.json', JSON.stringify(testData, null, 2));
writeFileSync('.generated/claim_details.json', JSON.stringify(claimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(contacts, null, 2));
// Verify files were written
log('✅ Verifying test data files...');
const files = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
for (const file of files) {
if (!existsSync(file)) {
throw new Error(`Failed to create ${file}`);
}
log(`✅ Created ${file}`);
}
log('✅ Test data generated successfully'); log('✅ Test data generated successfully');
} catch (error) { } catch (error) {
throw new Error(`Failed to generate test data: ${error.message}`); log(`Failed to generate test data: ${error.message}`);
throw error;
} }
}; };
@@ -116,14 +170,24 @@ const executeDeeplink = async (url, description, log) => {
try { try {
// Stop the app before executing the deep link // Stop the app before executing the deep link
execSync('adb shell am force-stop app.timesafari.app'); execSync('adb shell am force-stop app.timesafari');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`); execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
log(`✅ Successfully executed: ${description}`); log(`✅ Successfully executed: ${description}`);
// Wait between deeplink tests // Wait for app to load content
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s await new Promise(resolve => setTimeout(resolve, 3000));
// Wait for user confirmation before continuing
await question('\n⏎ Press Enter to continue to next test (or Ctrl+C to quit)...');
// Press Back button to ensure app is in consistent state
log(`📱 Sending keystroke (BACK) to device...`);
execSync('adb shell input keyevent KEYCODE_BACK');
// Small delay after keystroke
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) { } catch (error) {
log(`❌ Failed to execute deeplink: ${description}`); log(`❌ Failed to execute deeplink: ${description}`);
log(`Error: ${error.message}`); log(`Error: ${error.message}`);
@@ -137,11 +201,11 @@ const runDeeplinkTests = async (log) => {
try { try {
// Load test data // Load test data
const testEnv = parseEnvFile('.generated/test-env.sh'); const testEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
const claimDetails = JSON.parse(readFileSync('.generated/claim_details.json', 'utf8')); const claimDetails = JSON.parse(readFileSync('.generated/claim_details.json', 'utf8'));
const contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8')); const contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
// Test each deeplink // Test URLs
const deeplinkTests = [ const deeplinkTests = [
{ {
url: `timesafari://claim/${claimDetails.claim_id}`, url: `timesafari://claim/${claimDetails.claim_id}`,
@@ -173,26 +237,44 @@ const runDeeplinkTests = async (log) => {
} }
]; ];
// Show test plan
log('\n📋 Test Plan:');
deeplinkTests.forEach((test, i) => {
log(`${i + 1}. ${test.description}`);
});
// Execute each test // Execute each test
let testsCompleted = 0;
for (const test of deeplinkTests) { for (const test of deeplinkTests) {
// Show progress
log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
// Show upcoming test info
log('\n📱 NEXT TEST:');
log('------------------------');
log(`Description: ${test.description}`);
log(`URL: ${test.url}`);
log('------------------------');
await executeDeeplink(test.url, test.description, log); await executeDeeplink(test.url, test.description, log);
} testsCompleted++;
let succeeded = true; // If there are more tests, show the next one
try { if (testsCompleted < deeplinkTests.length) {
await executeDeeplink('timesafari://contactJunk', 'Non-existent deeplink', log); const nextTest = deeplinkTests[testsCompleted];
} catch (error) { log('\n⏭ NEXT UP:');
log('✅ Non-existent deeplink failed as expected'); log('------------------------');
succeeded = false; log(`Next test will be: ${nextTest.description}`);
} finally { log(`URL: ${nextTest.url}`);
if (succeeded) { log('------------------------');
throw new Error('Non-existent deeplink should have failed');
} }
} }
log(' All deeplink tests completed successfully'); log('\n🎉 All deeplink tests completed successfully!');
rl.close(); // Close readline interface when done
} catch (error) { } catch (error) {
log('❌ Deeplink tests failed'); log('❌ Deeplink tests failed');
rl.close(); // Close readline interface on error
throw error; throw error;
} }
}; };
@@ -214,24 +296,88 @@ const configureAndroidProject = async (log) => {
log('⚙️ Configuring Gradle properties...'); log('⚙️ Configuring Gradle properties...');
const gradleProps = 'android/gradle.properties'; const gradleProps = 'android/gradle.properties';
if (!existsSync(gradleProps) || !execSync(`grep -q "android.suppressUnsupportedCompileSdk=34" ${gradleProps}`)) {
// Create file if it doesn't exist
if (!existsSync(gradleProps)) {
execSync('touch android/gradle.properties');
}
// Check if line exists without using grep
const gradleContent = readFileSync(gradleProps, 'utf8');
if (!gradleContent.includes('android.suppressUnsupportedCompileSdk=34')) {
execSync('echo "android.suppressUnsupportedCompileSdk=34" >> android/gradle.properties'); execSync('echo "android.suppressUnsupportedCompileSdk=34" >> android/gradle.properties');
log('✅ Added SDK suppression to gradle.properties'); log('✅ Added SDK suppression to gradle.properties');
} else {
log('✅ SDK suppression already configured in gradle.properties');
} }
}; };
// Build and test Android project // Build and test Android project
const buildAndTestAndroid = async (log, env) => { const buildAndTestAndroid = async (log, env) => {
log('🏗️ Building Android project...'); log('🏗️ Building Android project...');
// Kill and restart ADB server first
try {
log('🔄 Restarting ADB server...');
execSync('adb kill-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s
execSync('adb start-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3s
// Verify device connection
const devices = execSync('adb devices').toString();
if (!devices.includes('\tdevice')) {
throw new Error('No devices connected after ADB restart');
}
log('✅ ADB server restarted successfully');
} catch (error) {
log(`⚠️ ADB restart failed: ${error.message}`);
log('Continuing with build process...');
}
// Clean build
log('🧹 Cleaning project...');
execSync('cd android && ./gradlew clean', { stdio: 'inherit', env }); execSync('cd android && ./gradlew clean', { stdio: 'inherit', env });
log('✅ Gradle clean completed'); log('✅ Gradle clean completed');
// Build
log('🏗️ Building project...');
execSync('cd android && ./gradlew build', { stdio: 'inherit', env }); execSync('cd android && ./gradlew build', { stdio: 'inherit', env });
log('✅ Gradle build completed'); log('✅ Gradle build completed');
// Run tests with retry
log('🧪 Running Android tests...'); log('🧪 Running Android tests...');
execSync('cd android && ./gradlew connectedAndroidTest', { stdio: 'inherit', env }); let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
// Verify ADB connection before tests
execSync('adb devices', { stdio: 'inherit' });
// Run the tests
execSync('cd android && ./gradlew connectedAndroidTest', {
stdio: 'inherit',
env,
timeout: 60000 // 1 minute timeout
});
log('✅ Android tests completed'); log('✅ Android tests completed');
return;
} catch (error) {
retryCount++;
log(`⚠️ Test attempt ${retryCount} failed: ${error.message}`);
if (retryCount < maxRetries) {
log('🔄 Restarting ADB and retrying...');
execSync('adb kill-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 2000));
execSync('adb start-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 3000));
} else {
throw new Error(`Android tests failed after ${maxRetries} attempts`);
}
}
}
}; };
// Run the app // Run the app
@@ -297,3 +443,9 @@ async function runAndroidTests() {
// Execute the test suite // Execute the test suite
runAndroidTests(); runAndroidTests();
// Add cleanup handler for SIGINT
process.on('SIGINT', () => {
rl.close();
process.exit();
});

View File

@@ -6,9 +6,11 @@
* web build and runs the test suite on a specified iOS simulator. * web build and runs the test suite on a specified iOS simulator.
* *
* Process flow: * Process flow:
* 1. Sync Capacitor project with latest web build * 1. Clean and reset iOS platform (if needed)
* 2. Build app for iOS simulator * 2. Check prerequisites (Xcode, CocoaPods, Capacitor setup)
* 3. Run XCTest suite * 3. Sync Capacitor project with latest web build
* 4. Build app for iOS simulator
* 5. Run XCTest suite
* *
* Prerequisites: * Prerequisites:
* - macOS operating system * - macOS operating system
@@ -38,7 +40,21 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { join } = require('path'); const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs'); const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, readdirSync, statSync, accessSync } = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const { constants } = require('fs');
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
// Make sure to close readline at the end
process.on('SIGINT', () => {
rl.close();
process.exit();
});
// Format date as YYYY-MM-DD-HHMMSS // Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => { const getLogFileName = () => {
@@ -58,6 +74,169 @@ const createLogger = (logFile) => {
}; };
}; };
/**
* Clean up and reset iOS platform
* This function completely removes and recreates the iOS platform to ensure a fresh setup
* @param {function} log - Logging function
* @returns {boolean} - Success status
*/
const cleanIosPlatform = async (log) => {
log('🧹 Cleaning iOS platform (complete reset)...');
// Check for package.json and capacitor.config.ts/js
if (!existsSync('package.json')) {
log('⚠️ package.json not found. Are you in the correct directory?');
throw new Error('package.json not found. Cannot continue without project configuration.');
}
log('✅ package.json exists');
const capacitorConfigExists =
existsSync('capacitor.config.ts') ||
existsSync('capacitor.config.js') ||
existsSync('capacitor.config.json');
if (!capacitorConfigExists) {
log('⚠️ Capacitor config file not found');
log('Creating minimal capacitor.config.ts...');
try {
// Get app name from package.json
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
const appName = packageJson.name || 'App';
const appId = packageJson.capacitor?.appId || 'io.ionic.starter';
// Create a minimal capacitor config
const capacitorConfig = `
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: '${appId}',
appName: '${appName}',
webDir: 'dist',
bundledWebRuntime: false
};
export default config;
`.trim();
writeFileSync('capacitor.config.ts', capacitorConfig);
log('✅ Created capacitor.config.ts');
} catch (configError) {
log('⚠️ Failed to create Capacitor config file');
log('Please create a capacitor.config.ts file manually');
throw new Error('Capacitor configuration missing. Please configure manually.');
}
} else {
log('✅ Capacitor config exists');
}
// Check if the platform exists first
if (existsSync('ios')) {
log('🗑️ Removing existing iOS platform directory...');
try {
execSync('rm -rf ios', { stdio: 'inherit' });
log('✅ Existing iOS platform removed');
} catch (error) {
log(`⚠️ Error removing iOS platform: ${error.message}`);
log('⚠️ You may need to manually remove the ios directory');
return false;
}
}
// Rebuild web assets first to ensure they're available
log('🔄 Building web assets before adding iOS platform...');
try {
execSync('rm -rf dist', { stdio: 'inherit' });
execSync('npm run build:web', { stdio: 'inherit' });
execSync('npm run build:capacitor', { stdio: 'inherit' });
log('✅ Web assets built successfully');
} catch (error) {
log(`⚠️ Error building web assets: ${error.message}`);
log('⚠️ Continuing with platform addition, but it may fail if web assets are required');
}
// Add the platform back
log(' Adding iOS platform...');
try {
execSync('npx cap add ios', { stdio: 'inherit' });
log('✅ iOS platform added successfully');
// Verify critical files were created
if (!existsSync('ios/App/Podfile')) {
log('⚠️ Podfile was not created - something is wrong with the Capacitor setup');
return false;
}
if (!existsSync('ios/App/App/Info.plist')) {
log('⚠️ Info.plist was not created - something is wrong with the Capacitor setup');
return false;
}
log('✅ iOS platform setup verified - critical files exist');
return true;
} catch (error) {
log(`⚠️ Error adding iOS platform: ${error.message}`);
return false;
}
};
/**
* Check all prerequisites for iOS testing
* Verifies and attempts to install/initialize all required components
*/
const checkPrerequisites = async (log) => {
log('🔍 Checking prerequisites for iOS testing...');
// Check for macOS
if (process.platform !== 'darwin') {
throw new Error('iOS testing is only supported on macOS');
}
log('✅ Running on macOS');
// Verify Xcode installation
try {
const xcodeOutput = execSync('xcode-select -p').toString().trim();
log(`✅ Xcode command line tools found at: ${xcodeOutput}`);
} catch (error) {
log('⚠️ Xcode command line tools not found');
log('Please install Xcode from the App Store and run:');
log('xcode-select --install');
throw new Error('Xcode command line tools not found. Please install Xcode first.');
}
// Check Xcode version
try {
const xcodeVersionOutput = execSync('xcodebuild -version').toString().trim();
log(`✅ Xcode version: ${xcodeVersionOutput.split('\n')[0]}`);
} catch (error) {
log('⚠️ Unable to determine Xcode version');
}
// Check for CocoaPods
try {
const podVersionOutput = execSync('pod --version').toString().trim();
log(`✅ CocoaPods version: ${podVersionOutput}`);
} catch (error) {
log('⚠️ CocoaPods not found');
log('Attempting to install CocoaPods...');
try {
log('🔄 Installing CocoaPods via gem...');
execSync('gem install cocoapods', { stdio: 'inherit' });
log('✅ CocoaPods installed successfully');
} catch (gemError) {
log('⚠️ Failed to install CocoaPods via gem');
log('Please install CocoaPods manually:');
log('1. sudo gem install cocoapods');
log('2. brew install cocoapods');
throw new Error('CocoaPods installation failed. Please install manually.');
}
}
log('✅ All prerequisites for iOS testing are met');
return true;
};
// Check for iOS simulator // Check for iOS simulator
const checkSimulator = async (log) => { const checkSimulator = async (log) => {
log('🔍 Checking for iOS simulator...'); log('🔍 Checking for iOS simulator...');
@@ -151,122 +330,84 @@ const verifyXcodeInstallation = (log) => {
// Generate test data using generate_data.ts // Generate test data using generate_data.ts
const generateTestData = async (log) => { const generateTestData = async (log) => {
log('🔄 Generating test data...'); log('\n🔍 DEBUG: Starting test data generation...');
// Check if test-scripts directory exists // Check directory structure
if (!existsSync('test-scripts')) { log('📁 Current directory:', process.cwd());
log('⚠️ test-scripts directory not found'); log('📁 Directory contents:', require('fs').readdirSync('.'));
log('⚠️ Current directory: ' + process.cwd());
// List directories to help debug
const { readdirSync } = require('fs');
log('📂 Directories in current path:');
try {
const files = readdirSync('.');
files.forEach(file => {
const isDir = existsSync(file) && require('fs').statSync(file).isDirectory();
log(`${isDir ? '📁' : '📄'} ${file}`);
});
} catch (err) {
log(`⚠️ Error listing directory: ${err.message}`);
}
} else {
log('✅ Found test-scripts directory');
// Check if generate_data.ts exists
if (existsSync('test-scripts/generate_data.ts')) {
log('✅ Found generate_data.ts');
} else {
log('⚠️ generate_data.ts not found in test-scripts directory');
// List files in test-scripts to help debug
const { readdirSync } = require('fs');
log('📂 Files in test-scripts:');
try {
const files = readdirSync('test-scripts');
files.forEach(file => {
log(`📄 ${file}`);
});
} catch (err) {
log(`⚠️ Error listing test-scripts: ${err.message}`);
}
}
}
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) { if (!existsSync('.generated')) {
log('📁 Creating .generated directory'); log('📁 Creating .generated directory');
mkdirSync('.generated', { recursive: true }); mkdirSync('.generated', { recursive: true });
} }
try { try {
// Try to generate test data using the script log('🔄 Attempting to run generate_data.ts...');
log('🔄 Running test data generation script...');
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' }); execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' });
log('✅ Test data generation script completed'); log('✅ Test data generation completed');
// Verify the generated files exist // Verify and log generated files content
const requiredFiles = [ const requiredFiles = [
'.generated/test-env.json', '.generated/test-env.json',
'.generated/claim_details.json', '.generated/claim_details.json',
'.generated/contacts.json' '.generated/contacts.json'
]; ];
log('🔍 Verifying generated files:'); log('\n📝 Verifying generated files:');
for (const file of requiredFiles) { for (const file of requiredFiles) {
if (!existsSync(file)) { if (!existsSync(file)) {
log(`⚠️ Required file ${file} was not generated`); log(`❌ Missing file: ${file}`);
throw new Error(`Required file ${file} was not generated`);
} else { } else {
log(`${file} exists`); const content = readFileSync(file, 'utf8');
log(`\n📄 Content of ${file}:`);
log(content);
try {
const parsed = JSON.parse(content);
if (file.includes('test-env.json')) {
log('🔑 CONTACT1_DID in test-env:', parsed.CONTACT1_DID);
}
if (file.includes('contacts.json')) {
log('👥 First contact DID:', parsed[0]?.did);
}
} catch (e) {
log(`❌ Error parsing ${file}:`, e);
}
} }
} }
} catch (error) { } catch (error) {
log(`⚠️ Failed to generate test data: ${error.message}`); log(`\n⚠️ Test data generation failed: ${error.message}`);
log('⚠️ Creating fallback test data...'); log('⚠️ Creating fallback test data...');
// Create minimal fallback test data // Create fallback data with detailed logging
const fallbackTestEnv = { const fallbackTestEnv = {
"CONTACT1_DID": "did:example:123456789", "CONTACT1_DID": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B",
"APP_URL": "https://app.timesafari.example" "APP_URL": "https://app.timesafari.example"
}; };
const fallbackClaimDetails = {
"claim_id": "claim_12345",
"title": "Test Claim",
"description": "This is a test claim"
};
const fallbackContacts = [ const fallbackContacts = [
{ {
"id": "contact1", "id": "contact1",
"name": "Test Contact", "name": "Test Contact",
"did": "did:example:123456789" "did": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B"
} }
]; ];
// Use writeFileSync to overwrite any existing files log('\n📝 Writing fallback data:');
const { writeFileSync } = require('fs'); log('TestEnv:', JSON.stringify(fallbackTestEnv, null, 2));
log('Contacts:', JSON.stringify(fallbackContacts, null, 2));
writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2)); writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2));
writeFileSync('.generated/claim_details.json', JSON.stringify(fallbackClaimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2)); writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2));
log('✅ Fallback test data created'); // Verify fallback data was written
log('\n🔍 Verifying fallback data:');
// Verify files were created try {
const requiredFiles = [ const writtenTestEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
'.generated/test-env.json', const writtenContacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
'.generated/claim_details.json', log('Written TestEnv:', writtenTestEnv);
'.generated/contacts.json' log('Written Contacts:', writtenContacts);
]; } catch (e) {
log('❌ Error verifying fallback data:', e);
log('🔍 Verifying fallback files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Failed to create ${file}`);
} else {
log(`✅ Created ${file}`);
}
} }
} }
}; };
@@ -282,40 +423,45 @@ const buildWebAssets = async (log) => {
// Configure iOS project // Configure iOS project
const configureIosProject = async (log) => { const configureIosProject = async (log) => {
log('📱 Syncing Capacitor project...'); log('📱 Configuring iOS project...');
try {
execSync('npx cap sync ios', { stdio: 'inherit' }); // Skip cap sync since we just did a clean platform add
log('✅ Capacitor sync completed'); log('✅ Using freshly created iOS platform');
} catch (error) {
log('⚠️ Capacitor sync encountered issues. Attempting to continue...');
}
// Register URL scheme for deeplink tests // Register URL scheme for deeplink tests
log('🔗 Configuring URL scheme for deeplink tests...'); log('🔗 Configuring URL scheme for deeplink tests...');
if (checkAndRegisterUrlScheme(log)) { if (checkAndRegisterUrlScheme(log)) {
log('✅ URL scheme configuration completed'); log('✅ URL scheme configuration completed');
} else {
log('⚠️ URL scheme could not be registered automatically');
log('⚠️ Deeplink tests may not work correctly');
} }
log('⚙️ Installing CocoaPods dependencies...'); log('⚙️ Installing CocoaPods dependencies...');
try { try {
// Try to run pod install normally first // Try to run pod install normally first
log('🔄 Running "pod install" in ios/App directory...');
execSync('cd ios/App && pod install', { stdio: 'inherit' }); execSync('cd ios/App && pod install', { stdio: 'inherit' });
} catch (error) {
// If that fails, try using sudo (requires password)
log('⚠️ CocoaPods installation failed. Trying with sudo...');
try {
execSync('cd ios/App && sudo pod install', { stdio: 'inherit' });
} catch (sudoError) {
// If both methods fail, alert the user
log('❌ CocoaPods installation failed.');
log('Please run one of the following commands manually:');
log('1. cd ios/App && pod install');
log('2. cd ios/App && sudo pod install');
log('3. Install CocoaPods through Homebrew: brew install cocoapods');
throw new Error('CocoaPods installation failed. See log for details.');
}
}
log('✅ CocoaPods installation completed'); log('✅ CocoaPods installation completed');
} catch (error) {
// If that fails, provide detailed instructions
log(`⚠️ CocoaPods installation failed: ${error.message}`);
log('⚠️ Please ensure CocoaPods is installed correctly:');
log('1. If using system Ruby: "sudo gem install cocoapods"');
log('2. If using Homebrew Ruby: "brew install cocoapods"');
log('3. Then run: "cd ios/App && pod install"');
// Try to continue despite the error
log('⚠️ Attempting to continue with the build process...');
}
// Add information about iOS security dialogs
log('\n📱 iOS Security Dialog Information:');
log('⚠️ iOS will display security confirmation dialogs when testing deeplinks');
log('⚠️ This is a security feature of iOS and cannot be bypassed in normal testing');
log('⚠️ You will need to manually approve each deeplink test by clicking "Open" in the dialog');
log('⚠️ The app must be running in the foreground for deeplinks to work properly');
log('⚠️ If tests appear to hang, check if a security dialog is waiting for your confirmation');
}; };
// Build and test iOS project // Build and test iOS project
@@ -365,6 +511,96 @@ const runIosApp = async (log, simulator) => {
log('✅ App launched successfully'); log('✅ App launched successfully');
}; };
const validateTestData = (log) => {
log('\n=== VALIDATING TEST DATA ===');
const generateFreshTestData = () => {
log('\n🔄 Generating fresh test data...');
try {
// Ensure .generated directory exists
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
// Execute the generate_data.ts script synchronously
log('Running generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', {
stdio: 'inherit',
encoding: 'utf8'
});
// Read and validate the generated files
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
throw new Error('Generated files not found after running generate_data.ts');
}
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CONTACT1_DID) {
throw new Error('CONTACT1_DID missing from generated test data');
}
log('Generated test data:', {
testEnv: testEnv,
contacts: contacts
});
return { testEnv, contacts };
} catch (error) {
log('❌ Test data generation failed:', error);
throw error;
}
};
try {
// Try to read existing data or generate fresh data
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
let testData;
// If either file is missing or invalid, generate fresh data
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
testData = generateFreshTestData();
} else {
try {
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CLAIM_ID || !testEnv.CONTACT1_DID) {
log('⚠️ Existing test data missing required fields, regenerating...');
testData = generateFreshTestData();
} else {
testData = { testEnv, contacts };
}
} catch (error) {
log('⚠️ Error reading existing test data, regenerating...');
testData = generateFreshTestData();
}
}
// Final validation of data
if (!testData.testEnv.CLAIM_ID || !testData.testEnv.CONTACT1_DID) {
throw new Error('Test data validation failed even after generation');
}
log('✅ Test data validated successfully');
log('📄 Test Environment:', JSON.stringify(testData.testEnv, null, 2));
return testData;
} catch (error) {
log(`❌ Test data validation failed: ${error.message}`);
throw error;
}
};
/** /**
* Run deeplink tests * Run deeplink tests
* Optionally tests deeplinks if the test data is available * Optionally tests deeplinks if the test data is available
@@ -373,87 +609,31 @@ const runIosApp = async (log, simulator) => {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const runDeeplinkTests = async (log) => { const runDeeplinkTests = async (log) => {
log('🔗 Starting deeplink tests...'); log('\n=== Starting Deeplink Tests ===');
// Register URL scheme if needed
checkAndRegisterUrlScheme(log);
// Check if test data files exist first
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} does not exist`);
log('⚠️ Skipping deeplink tests');
return;
}
}
// Validate test data before proceeding
let testEnv, contacts;
try { try {
// Load test data ({ testEnv, contacts } = validateTestData(log));
log('📂 Loading test data from .generated directory');
let testEnv, claimDetails, contacts;
try {
const testEnvContent = readFileSync('.generated/test-env.json', 'utf8');
testEnv = JSON.parse(testEnvContent);
log('✅ Loaded test-env.json');
} catch (error) { } catch (error) {
log(`⚠️ Failed to load test-env.json: ${error.message}`); log('❌ Cannot proceed with tests due to invalid test data');
return; log(`Error: ${error.message}`);
log('Please ensure test data is properly generated before running tests');
process.exit(1); // Exit with error code
} }
try { // Now we can safely create the deeplink tests knowing we have valid data
const claimDetailsContent = readFileSync('.generated/claim_details.json', 'utf8');
claimDetails = JSON.parse(claimDetailsContent);
log('✅ Loaded claim_details.json');
} catch (error) {
log(`⚠️ Failed to load claim_details.json: ${error.message}`);
return;
}
try {
const contactsContent = readFileSync('.generated/contacts.json', 'utf8');
contacts = JSON.parse(contactsContent);
log('✅ Loaded contacts.json');
} catch (error) {
log(`⚠️ Failed to load contacts.json: ${error.message}`);
return;
}
// Check if the app URL scheme is registered in the simulator
log('🔍 Checking if URL scheme is registered in simulator...');
try {
// Attempt to open a simple URL with the scheme
execSync(`xcrun simctl openurl booted "timesafari://test"`, { stdio: 'pipe' });
log('✅ URL scheme is registered and working');
} catch (error) {
const errorMessage = error.message || '';
// Check for the specific error code that indicates an unregistered URL scheme
if (errorMessage.includes('OSStatus error -10814') || errorMessage.includes('NSOSStatusErrorDomain, code=-10814')) {
log('⚠️ URL scheme "timesafari://" is not registered in the app or app is not running');
log('⚠️ The scheme was added to Info.plist but the app may need to be rebuilt');
log('⚠️ Trying to continue with tests, but they may fail');
}
}
// Test URLs
const deeplinkTests = [ const deeplinkTests = [
{ {
url: `timesafari://claim/${claimDetails.claim_id}`, url: `timesafari://claim/${testEnv.CLAIM_ID}`,
description: 'Claim view' description: 'Claim view'
}, },
{ {
url: `timesafari://claim-cert/${claimDetails.claim_id}`, url: `timesafari://claim-cert/${testEnv.CERT_ID || testEnv.CLAIM_ID}`,
description: 'Claim certificate view' description: 'Claim certificate view'
}, },
{ {
url: `timesafari://claim-add-raw/${claimDetails.claim_id}`, url: `timesafari://claim-add-raw/${testEnv.RAW_CLAIM_ID || testEnv.CLAIM_ID}`,
description: 'Raw claim addition' description: 'Raw claim addition'
}, },
{ {
@@ -465,7 +645,14 @@ const runDeeplinkTests = async (log) => {
description: 'DID view with contact DID' description: 'DID view with contact DID'
}, },
{ {
url: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`, url: (() => {
if (!testEnv?.CONTACT1_DID) {
throw new Error('Cannot construct contact-edit URL: CONTACT1_DID is missing');
}
const url = `timesafari://contact-edit/${testEnv.CONTACT1_DID}`;
log('Created contact-edit URL:', url);
return url;
})(),
description: 'Contact editing' description: 'Contact editing'
}, },
{ {
@@ -474,21 +661,63 @@ const runDeeplinkTests = async (log) => {
} }
]; ];
// Log the final test configuration
log('\n5. Final Test Configuration:');
deeplinkTests.forEach((test, i) => {
log(`\nTest ${i + 1}:`);
log(`Description: ${test.description}`);
log(`URL: ${test.url}`);
});
// Show instructions for iOS security dialogs
log('\n📱 IMPORTANT: iOS Security Dialog Instructions:');
log('1. Each deeplink test will trigger a security confirmation dialog');
log('2. You MUST click "Open" on each dialog to continue testing');
log('3. The app must be running in the FOREGROUND');
log('4. You will need to press Enter in this terminal after handling each dialog');
log('5. You can abort the testing process by pressing Ctrl+C\n');
// Ensure app is in foreground
log('⚠️ IMPORTANT: Please make sure the app is in the FOREGROUND now');
await question('Press Enter when the app is visible and in the foreground...');
try {
// Execute each test // Execute each test
let testsCompleted = 0; let testsCompleted = 0;
let testsSkipped = 0; let testsSkipped = 0;
for (const test of deeplinkTests) { for (const test of deeplinkTests) {
// Show upcoming test info before execution
log('\n📱 NEXT TEST:');
log('------------------------');
log(`Description: ${test.description}`);
log(`URL to test: ${test.url}`);
log('------------------------');
// Clear prompt for user action
await question('\n⏎ Press Enter to execute this test (or Ctrl+C to quit)...');
try { try {
log(`\n🔗 Testing deeplink: ${test.description}`); log('🚀 Executing deeplink test...');
log(`URL: ${test.url}`); log('⚠️ iOS SECURITY DIALOG WILL APPEAR - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' }); execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' });
log(`✅ Successfully executed: ${test.description}`); log(`✅ Successfully executed: ${test.description}`);
testsCompleted++; testsCompleted++;
// Wait between tests // Show progress
await new Promise(resolve => setTimeout(resolve, 5000)); log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
// If there are more tests, show the next one
if (testsCompleted < deeplinkTests.length) {
const nextTest = deeplinkTests[testsCompleted];
log('\n⏭ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
} catch (deeplinkError) { } catch (deeplinkError) {
const errorMessage = deeplinkError.message || ''; const errorMessage = deeplinkError.message || '';
@@ -501,22 +730,35 @@ const runDeeplinkTests = async (log) => {
log(`⚠️ Error: ${errorMessage}`); log(`⚠️ Error: ${errorMessage}`);
} }
log('⚠️ Continuing with next test...'); log('⚠️ Continuing with next test...');
// Show next test info after error handling
if (testsCompleted + testsSkipped < deeplinkTests.length) {
const nextTest = deeplinkTests[testsCompleted + testsSkipped];
log('\n⏭ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
} }
} }
log(`✅ Deeplink tests completed: ${testsCompleted} successful, ${testsSkipped} skipped`); log('\n🎉 All deeplink tests completed!');
log(`✅ Successful: ${testsCompleted}`);
log(`⚠️ Skipped: ${testsSkipped}`);
if (testsSkipped > 0) { if (testsSkipped > 0) {
log('\n📝 Note about skipped tests:'); log('\n📝 Note about skipped tests:');
log('1. The app needs to have the URL scheme registered in Info.plist'); log('1. The app needs to have the URL scheme registered in Info.plist');
log('2. The app needs to be rebuilt after registering the URL scheme'); log('2. The app needs to be rebuilt after registering the URL scheme');
log('3. The app must be running in the foreground for deeplink tests to work'); log('3. The app must be running in the foreground for deeplink tests to work');
log('4. If these conditions are met and tests still fail, check URL handling in the app code'); log('4. iOS security dialogs must be manually approved for each deeplink test');
log('5. If these conditions are met and tests still fail, check URL handling in the app code');
} }
} catch (error) { } catch (error) {
log(`❌ Deeplink tests setup failed: ${error.message}`); log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing'); log('⚠️ Deeplink tests might be unavailable or test data is missing');
// Don't rethrow the error to prevent halting the process
} }
}; };
@@ -558,7 +800,7 @@ const checkAndRegisterUrlScheme = (log) => {
<array> <array>
<dict> <dict>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>app.timesafari.app</string> <string>app.timesafari</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>timesafari</string> <string>timesafari</string>
@@ -585,14 +827,51 @@ const checkAndRegisterUrlScheme = (log) => {
} }
}; };
// Helper function to get the app identifier from package.json or capacitor config
const getAppIdentifier = () => {
try {
// Try to read from capacitor.config.ts/js/json
if (existsSync('capacitor.config.json')) {
const config = JSON.parse(readFileSync('capacitor.config.json', 'utf8'));
return config.appId;
}
if (existsSync('capacitor.config.js')) {
// We can't directly require the file, but we can try to extract the appId
const content = readFileSync('capacitor.config.js', 'utf8');
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
if (match && match[1]) return match[1];
}
if (existsSync('capacitor.config.ts')) {
// Similar approach for TypeScript
const content = readFileSync('capacitor.config.ts', 'utf8');
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
if (match && match[1]) return match[1];
}
// Fall back to package.json
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
if (packageJson.capacitor && packageJson.capacitor.appId) {
return packageJson.capacitor.appId;
}
// Default fallback
return 'app.timesafari';
} catch (error) {
console.error('Error getting app identifier:', error);
return 'app.timesafari'; // Default fallback
}
};
/** /**
* Runs the complete iOS test suite including build and testing * Runs the complete iOS test suite including build and testing
* *
* The function performs the following steps: * The function performs the following steps:
* 1. Syncs the Capacitor project with latest web build * 1. Cleans and resets the iOS platform
* 2. Builds the app using xcodebuild * 2. Verifies prerequisites and project setup
* 3. Optionally runs tests if configured * 3. Syncs the Capacitor project with latest web build
* 4. Launches the app in the simulator * 4. Builds the app using xcodebuild
* 5. Optionally runs tests if configured
* 6. Launches the app in the simulator
* *
* If no simulator is running, it automatically selects and boots one. * If no simulator is running, it automatically selects and boots one.
* *
@@ -617,7 +896,16 @@ async function runIosTests() {
try { try {
log('🚀 Starting iOS build and test process...'); log('🚀 Starting iOS build and test process...');
// Generate test data first // Clean and reset iOS platform first
const cleanSuccess = await cleanIosPlatform(log);
if (!cleanSuccess) {
throw new Error('Failed to clean and reset iOS platform. Please check the logs for details.');
}
// Check prerequisites
await checkPrerequisites(log);
// Generate test data
await generateTestData(log); await generateTestData(log);
// Verify Xcode installation // Verify Xcode installation
@@ -626,8 +914,7 @@ async function runIosTests() {
// Check for simulator or boot one if needed // Check for simulator or boot one if needed
const simulator = await checkSimulator(log); const simulator = await checkSimulator(log);
// Build web assets and configure iOS project // Configure iOS project
await buildWebAssets(log);
await configureIosProject(log); await configureIosProject(log);
// Build and test using the selected simulator // Build and test using the selected simulator

View File

@@ -0,0 +1,265 @@
<template>
<li>
<!-- Last viewed separator -->
<div
v-if="record.jwtId == lastViewedClaimId"
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm"
>
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following
</span>
</div>
<div class="bg-slate-100 rounded-t-md border border-slate-300 p-3 sm:p-4">
<div class="flex items-center gap-2 mb-6">
<div v-if="record.issuerDid">
<EntityIcon
:entity-id="record.issuerDid"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</div>
<div v-else>
<font-awesome
icon="person-circle-question"
class="text-slate-300 text-[2rem]"
/>
</div>
<div>
<h3 class="font-semibold">
{{ record.issuer.known ? record.issuer.displayName : "" }}
</h3>
<p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }}
</p>
</div>
</div>
<!-- Record Image -->
<div
v-if="record.image"
class="bg-cover mb-6 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`"
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@click="$emit('viewImage', record.image)"
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image"
alt="Activity image"
@load="$emit('cacheImage', record.image)"
/>
</a>
</div>
<div class="relative flex justify-between gap-4 max-w-lg 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"
>
<div class="relative w-fit mx-auto">
<div>
<!-- Project Icon -->
<div v-if="record.providerPlanName">
<ProjectIcon
:entity-id="record.providerPlanName"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</div>
<!-- Identicon for DIDs -->
<div v-else-if="record.agentDid">
<EntityIcon
:entity-id="record.agentDid"
:profile-image-url="record.issuer.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</div>
<!-- Unknown Person -->
<div v-else>
<font-awesome
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</div>
</div>
</div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2">
<div v-if="record.providerPlanName || record.giver.known">
<font-awesome
:icon="record.providerPlanName ? 'users' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.providerPlanName || record.giver.displayName }}
</div>
</div>
</div>
<!-- Arrow -->
<div
class="absolute inset-x-28 sm:inset-x-40 mx-2 top-1/2 -translate-y-1/2"
>
<div class="text-sm text-center leading-none font-semibold">
{{ fetchAmount }}
</div>
<div class="flex items-center">
<hr
class="grow border-t-[18px] sm:border-t-[24px] border-slate-300"
/>
<div
class="shrink-0 w-0 h-0 border border-slate-300 border-t-[20px] sm:border-t-[25px] border-t-transparent border-b-[20px] sm:border-b-[25px] border-b-transparent border-s-[27px] sm:border-s-[34px] border-e-0"
></div>
</div>
</div>
<!-- Destination -->
<div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
>
<div class="relative w-fit mx-auto">
<div>
<!-- Project Icon -->
<div v-if="record.recipientProjectName">
<ProjectIcon
:entity-id="record.recipientProjectName"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</div>
<!-- Identicon for DIDs -->
<div v-else-if="record.recipientDid">
<EntityIcon
:entity-id="record.recipientDid"
:profile-image-url="record.receiver.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</div>
<!-- Unknown Person -->
<div v-else>
<font-awesome
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</div>
</div>
</div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2">
<div v-if="record.recipientProjectName || record.receiver.known">
<font-awesome
:icon="record.recipientProjectName ? 'users' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.recipientProjectName || record.receiver.displayName }}
</div>
</div>
</div>
</div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
</div>
<div
class="flex items-center gap-2 text-lg bg-slate-300 rounded-b-md px-3 sm:px-4 py-1 sm:py-2"
>
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a>
</div>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class ActivityListItem extends Vue {
@Prop() record!: GiveRecordWithContactInfo;
@Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
get fetchAmount(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
const amount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: "";
return amount;
}
get description(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
if (!claim.description) {
return "something not described";
}
return `${claim.description}`;
}
private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
}
private currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
get canConfirm(): boolean {
if (!this.isRegistered) return false;
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
if (this.confirmerIdList?.includes(this.activeDid)) return false;
if (this.record.issuerDid === this.activeDid) return false;
if (containsHiddenDid(this.record.fullClaim)) return false;
return true;
}
handleConfirmClick() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
this.$notify,
this.isRegistered,
this.record.fullClaim?.["@type"],
this.record,
this.activeDid,
this.confirmerIdList,
);
return;
}
this.$emit("confirmClaim", this.record);
}
get friendlyDate(): string {
const date = new Date(this.record.issuedAt);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
}
</script>

View File

@@ -28,7 +28,7 @@
:src="imageUrl" :src="imageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain" class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content" alt="expanded shared content"
@click.stop @click="close"
/> />
</div> </div>
</div> </div>

View File

@@ -112,7 +112,6 @@ db.on("populate", async () => {
// ended up throwing lots of errors to the user... and they'd end up in a state // ended up throwing lots of errors to the user... and they'd end up in a state
// where they couldn't take action because they couldn't unlock that identity.) // where they couldn't take action because they couldn't unlock that identity.)
// check for the secret in storage
async function useSecretAndInitializeAccountsDB( async function useSecretAndInitializeAccountsDB(
secretDB: SecretDexie, secretDB: SecretDexie,
accountsDB: SensitiveDexie, accountsDB: SensitiveDexie,
@@ -214,6 +213,22 @@ export async function updateAccountSettings(
} }
} }
export async function logToDb(message: string): Promise<void> {
await db.open();
const todayKey = new Date().toDateString();
// only keep one day's worth of logs
const previous = await db.logs.get(todayKey);
if (!previous) {
// when this is today's first log, clear out everything previous
// to avoid the log table getting too large
// (let's limit a different way someday)
await db.logs.clear();
}
const prevMessages = (previous && previous.message) || "";
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`;
await db.logs.update(todayKey, { message: fullMessage });
}
// similar method is in the sw_scripts/additional-scripts.js file // similar method is in the sw_scripts/additional-scripts.js file
export async function logConsoleAndDb( export async function logConsoleAndDb(
message: string, message: string,
@@ -224,16 +239,5 @@ export async function logConsoleAndDb(
} else { } else {
logger.log(`${new Date().toISOString()} ${message}`); logger.log(`${new Date().toISOString()} ${message}`);
} }
await logToDb(message);
await db.open();
const todayKey = new Date().toDateString();
// only keep one day's worth of logs
const previous = await db.logs.get(todayKey);
if (!previous) {
// when this is today's first log, clear out everything previous
await db.logs.clear();
}
const prevMessages = (previous && previous.message) || "";
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`;
await db.logs.update(todayKey, { message: fullMessage });
} }

View File

@@ -0,0 +1,37 @@
/**
* Electron-specific logger implementation
*/
const fs = require("fs");
const path = require("path");
const { app } = require("electron");
// Create logs directory if it doesn't exist
const logsDir = path.join(app.getPath("userData"), "logs");
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const logFile = path.join(
logsDir,
`electron-${new Date().toISOString().split("T")[0]}.log`,
);
function log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
// Write to log file
fs.appendFileSync(logFile, logMessage);
// Also output to console
// eslint-disable-next-line no-console
console[level.toLowerCase()](message);
}
module.exports = {
info: (message) => log("INFO", message),
warn: (message) => log("WARN", message),
error: (message) => log("ERROR", message),
debug: (message) => log("DEBUG", message),
getLogPath: () => logFile,
};

View File

@@ -1,174 +1,236 @@
const { app, BrowserWindow } = require("electron"); const { app, BrowserWindow, session, protocol, dialog } = require("electron");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const logger = require("../utils/logger");
// Check if running in dev mode // Global window reference
const isDev = process.argv.includes("--inspect"); let mainWindow = null;
// Debug flags
const isDev = !app.isPackaged;
// Helper for logging
function logDebug(...args) {
// eslint-disable-next-line no-console
console.log("[DEBUG]", ...args);
}
function logError(...args) {
// eslint-disable-next-line no-console
console.error("[ERROR]", ...args);
if (!isDev && mainWindow) {
dialog.showErrorBox("TimeSafari Error", args.join(" "));
}
}
// Get the most appropriate app path
function getAppPath() {
if (app.isPackaged) {
const possiblePaths = [
path.join(process.resourcesPath, "app.asar", "dist-electron"),
path.join(process.resourcesPath, "app.asar"),
path.join(process.resourcesPath, "app"),
app.getAppPath(),
];
for (const testPath of possiblePaths) {
const testFile = path.join(testPath, "www", "index.html");
if (fs.existsSync(testFile)) {
logDebug(`Found valid app path: ${testPath}`);
return testPath;
}
}
logError("Could not find valid app path");
return path.join(process.resourcesPath, "app.asar"); // Default fallback
} else {
return __dirname;
}
}
// Create the browser window
function createWindow() { function createWindow() {
// Add before createWindow function logDebug("Creating window with paths:");
const preloadPath = path.join(__dirname, "preload.js"); logDebug("- process.resourcesPath:", process.resourcesPath);
logger.log("Checking preload path:", preloadPath); logDebug("- app.getAppPath():", app.getAppPath());
logger.log("Preload exists:", fs.existsSync(preloadPath)); logDebug("- __dirname:", __dirname);
// Create the browser window. // Create the browser window
const mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
webPreferences: { webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
allowRunningInsecureContent: false,
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
}, },
}); });
// Always open DevTools for now // Fix root file paths - replaces all protocol handling
mainWindow.webContents.openDevTools(); protocol.interceptFileProtocol("file", (request, callback) => {
let urlPath = request.url.substr(7); // Remove 'file://' prefix
urlPath = decodeURIComponent(urlPath); // Handle special characters
// Intercept requests to fix asset paths // Debug all asset requests
mainWindow.webContents.session.webRequest.onBeforeRequest( if (
{ urlPath.includes("assets/") ||
urls: [ urlPath.endsWith(".js") ||
"file://*/*/assets/*", urlPath.endsWith(".css") ||
"file://*/assets/*", urlPath.endsWith(".html")
"file:///assets/*", // Catch absolute paths ) {
"<all_urls>", // Catch all URLs as a fallback logDebug(`Intercepted request for: ${urlPath}`);
],
},
(details, callback) => {
let url = details.url;
// Handle paths that don't start with file://
if (!url.startsWith("file://") && url.includes("/assets/")) {
url = `file://${path.join(__dirname, "www", url)}`;
} }
// Handle absolute paths starting with /assets/ // Fix paths for files at root like registerSW.js or manifest.webmanifest
if (url.includes("/assets/") && !url.includes("/www/assets/")) { if (
const baseDir = url.includes("dist-electron") urlPath.endsWith("registerSW.js") ||
? url.substring( urlPath.endsWith("manifest.webmanifest") ||
0, urlPath.endsWith("sw.js")
url.indexOf("/dist-electron") + "/dist-electron".length, ) {
) const appBasePath = getAppPath();
: `file://${__dirname}`; const filePath = path.join(appBasePath, "www", path.basename(urlPath));
const assetPath = url.split("/assets/")[1];
const newUrl = `${baseDir}/www/assets/${assetPath}`;
callback({ redirectURL: newUrl });
return;
}
callback({}); // No redirect for other URLs if (fs.existsSync(filePath)) {
}, logDebug(`Serving ${urlPath} from ${filePath}`);
return callback({ path: filePath });
} else {
// For service worker, provide empty content to avoid errors
if (urlPath.endsWith("registerSW.js") || urlPath.endsWith("sw.js")) {
logDebug(`Providing empty SW file for ${urlPath}`);
// Create an empty JS file content that does nothing
const tempFile = path.join(
app.getPath("temp"),
path.basename(urlPath),
); );
fs.writeFileSync(
if (isDev) { tempFile,
// Debug info "// Service workers disabled in Electron\n",
logger.log("Debug Info:"); );
logger.log("Running in dev mode:", isDev); return callback({ path: tempFile });
logger.log("App is packaged:", app.isPackaged); }
logger.log("Process resource path:", process.resourcesPath); }
logger.log("App path:", app.getAppPath());
logger.log("__dirname:", __dirname);
logger.log("process.cwd():", process.cwd());
} }
const indexPath = path.join(__dirname, "www", "index.html"); // Handle assets paths that might be requested from root
if (urlPath.startsWith("/assets/") || urlPath === "/assets") {
if (isDev) { const appBasePath = getAppPath();
logger.log("Loading index from:", indexPath); const filePath = path.join(appBasePath, "www", urlPath);
logger.log("www path:", path.join(__dirname, "www")); logDebug(`Redirecting ${urlPath} to ${filePath}`);
logger.log("www assets path:", path.join(__dirname, "www", "assets")); return callback({ path: filePath });
} }
if (!fs.existsSync(indexPath)) { // Handle assets paths that are missing the www folder
logger.error(`Index file not found at: ${indexPath}`); if (urlPath.includes("/assets/")) {
throw new Error("Index file not found"); const appBasePath = getAppPath();
const relativePath = urlPath.substring(urlPath.indexOf("/assets/"));
const filePath = path.join(appBasePath, "www", relativePath);
if (fs.existsSync(filePath)) {
logDebug(`Fixing asset path ${urlPath} to ${filePath}`);
return callback({ path: filePath });
}
} }
// Add CSP headers to allow API connections // For all other paths, just pass them through
mainWindow.webContents.session.webRequest.onHeadersReceived( callback({ path: urlPath });
(details, callback) => { });
// Set up CSP headers - more permissive in dev mode
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({ callback({
responseHeaders: { responseHeaders: {
...details.responseHeaders, ...details.responseHeaders,
"Content-Security-Policy": [ "Content-Security-Policy": [
"default-src 'self';" + isDev
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" + ? "default-src 'self' file:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://*; connect-src 'self' https://*"
"img-src 'self' data: https: blob:;" + : "default-src 'self' file:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.timesafari.app https://*.americancloud.com; connect-src 'self' https://api.timesafari.app https://api.endorser.ch https://test-api.endorser.ch https://fonts.googleapis.com",
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
"style-src 'self' 'unsafe-inline';" +
"font-src 'self' data:;",
], ],
}, },
}); });
}, });
// Load the index.html with modifications
try {
const appPath = getAppPath();
const wwwFolder = path.join(appPath, "www");
const indexPath = path.join(wwwFolder, "index.html");
logDebug("Loading app from:", indexPath);
// Check if the file exists
if (fs.existsSync(indexPath)) {
// Read and modify index.html to disable service worker
let indexContent = fs.readFileSync(indexPath, "utf8");
// 1. Add base tag for proper path resolution
indexContent = indexContent.replace(
"<head>",
`<head>\n <base href="file://${wwwFolder}/">`,
); );
// Load the index.html // 2. Disable service worker registration by replacing the script
mainWindow if (indexContent.includes("registerSW.js")) {
.loadFile(indexPath) indexContent = indexContent.replace(
.then(() => { /<script src="registerSW\.js"><\/script>/,
logger.log("Successfully loaded index.html"); "<script>/* Service worker disabled in Electron */</script>",
if (isDev) { );
mainWindow.webContents.openDevTools();
logger.log("DevTools opened - running in dev mode");
} }
})
.catch((err) => {
logger.error("Failed to load index.html:", err);
logger.error("Attempted path:", indexPath);
});
// Listen for console messages from the renderer // Create a temp file with modified content
mainWindow.webContents.on("console-message", (_event, level, message) => { const tempDir = app.getPath("temp");
logger.log("Renderer Console:", message); const tempIndexPath = path.join(tempDir, "timesafari-index.html");
}); fs.writeFileSync(tempIndexPath, indexContent);
// Add right after creating the BrowserWindow // Load the modified index.html
mainWindow.webContents.on( mainWindow.loadFile(tempIndexPath).catch((err) => {
"did-fail-load", logError("Failed to load via loadFile:", err);
(event, errorCode, errorDescription) => {
logger.error("Page failed to load:", errorCode, errorDescription); // Fallback to direct URL loading
}, mainWindow.loadURL(`file://${tempIndexPath}`).catch((err2) => {
logError("Both loading methods failed:", err2);
mainWindow.loadURL(
"data:text/html,<h1>Error: Failed to load TimeSafari</h1><p>Please contact support.</p>",
); );
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => {
logger.error("Preload script error:", preloadPath, error);
}); });
});
mainWindow.webContents.on( } else {
"console-message", logError(`Index file not found at: ${indexPath}`);
(event, level, message, line, sourceId) => { mainWindow.loadURL(
logger.log("Renderer Console:", line, sourceId, message); "data:text/html,<h1>Error: Cannot find application</h1><p>index.html not found</p>",
},
); );
}
} catch (err) {
logError("Failed to load app:", err);
}
// Enable remote debugging when in dev mode // Open DevTools in development
if (isDev) { if (isDev) {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
mainWindow.on("closed", () => {
mainWindow = null;
});
} }
// Handle app ready // App lifecycle events
app.whenReady().then(createWindow); app.whenReady().then(() => {
logDebug(`Starting TimeSafari v${app.getVersion()}`);
// Handle all windows closed // Skip the service worker registration for file:// protocol
app.on("window-all-closed", () => { process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); createWindow();
}
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
}); });
// Handle any errors app.on("window-all-closed", () => {
process.on("uncaughtException", (error) => { if (process.platform !== "darwin") app.quit();
logger.error("Uncaught Exception:", error); });
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
logError("Uncaught Exception:", error);
}); });

View File

@@ -1,78 +1,95 @@
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require("electron");
const logger = { // Safety wrapper for logging
log: (message, ...args) => { function safeLog(message) {
if (process.env.NODE_ENV !== "production") { try {
/* eslint-disable no-console */ // eslint-disable-next-line no-console
console.log(message, ...args); console.log("[Preload]", message);
/* eslint-enable no-console */ } catch (e) {
// Silent fail for logging
} }
}, }
warn: (message, ...args) => {
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.warn(message, ...args);
/* eslint-enable no-console */
}
},
error: (message, ...args) => {
/* eslint-disable no-console */
console.error(message, ...args); // Errors should always be logged
/* eslint-enable no-console */
},
};
// Use a more direct path resolution approach // Initialize
const getPath = (pathType) => { safeLog("Preload script starting...");
switch (pathType) {
case "userData":
return (
process.env.APPDATA ||
(process.platform === "darwin"
? `${process.env.HOME}/Library/Application Support`
: `${process.env.HOME}/.local/share`)
);
case "home":
return process.env.HOME;
case "appPath":
return process.resourcesPath;
default:
return "";
}
};
logger.log("Preload script starting...");
try { try {
contextBridge.exposeInMainWorld("electronAPI", { // Mock service worker registration to prevent errors
// Path utilities if (window.navigator) {
getPath, // Override the service worker registration to return a fake promise that resolves with nothing
window.navigator.serviceWorker = {
register: () => Promise.resolve({}),
getRegistration: () => Promise.resolve(null),
ready: Promise.resolve({}),
};
}
// IPC functions // Safely expose specific APIs to the renderer process
contextBridge.exposeInMainWorld("electronAPI", {
// Basic flags/info
isElectron: true,
// Disable service worker in Electron
disableServiceWorker: true,
// Logging
log: (message) => {
try {
// eslint-disable-next-line no-console
console.log("[Renderer]", message);
} catch (e) {
// Silence any errors from logging
}
},
// Report errors to main process
reportError: (error) => {
try {
ipcRenderer.send("app-error", error.toString());
} catch (e) {
// eslint-disable-next-line no-console
console.error("Failed to report error to main process", e);
}
},
// Safe path handling helper (no Node modules needed)
joinPath: (...parts) => {
return parts.join("/").replace(/\/\//g, "/");
},
// Fix asset URLs
resolveAssetUrl: (assetPath) => {
if (assetPath.startsWith("/assets/")) {
return assetPath; // Already properly formed
}
if (assetPath.startsWith("assets/")) {
return "/" + assetPath; // Add leading slash
}
return assetPath;
},
// Send messages to main process
send: (channel, data) => { send: (channel, data) => {
const validChannels = ["toMain"]; // Whitelist channels for security
const validChannels = ["app-event", "log-event", "app-error"];
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data); ipcRenderer.send(channel, data);
} }
}, },
// Receive messages from main process
receive: (channel, func) => { receive: (channel, func) => {
const validChannels = ["fromMain"]; const validChannels = ["app-notification", "log-response"];
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args)); // Remove old listeners to avoid memory leaks
ipcRenderer.removeAllListeners(channel);
// Add the new listener
ipcRenderer.on(channel, (_, ...args) => func(...args));
} }
}, },
// Environment info
env: {
isElectron: true,
isDev: process.env.NODE_ENV === "development",
},
// Path utilities
getBasePath: () => {
return process.env.NODE_ENV === "development" ? "/" : "./";
},
}); });
logger.log("Preload script completed successfully"); safeLog("Preload script completed successfully");
} catch (error) { } catch (err) {
logger.error("Error in preload script:", error); safeLog("Error in preload script: " + err.toString());
} }

View File

@@ -99,7 +99,6 @@ export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0; return isNumeric(str) ? +str : 0;
} }
/** /**
* from https://tools.ietf.org/html/rfc3986#section-3 * from https://tools.ietf.org/html/rfc3986#section-3
* also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition * also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition

View File

@@ -7,7 +7,169 @@ import axios from "axios";
import VueAxios from "vue-axios"; import VueAxios from "vue-axios";
import Notifications from "notiwind"; import Notifications from "notiwind";
import "./assets/styles/tailwind.css"; import "./assets/styles/tailwind.css";
import { FontAwesomeIcon } from "./lib/fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImage,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faRightFromBracket,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
library.add(
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImage,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faQuestion,
faRotate,
faRightFromBracket,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
);
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Camera from "simple-vue-camera"; import Camera from "simple-vue-camera";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";

View File

@@ -2,38 +2,66 @@
import { register } from "register-service-worker"; import { register } from "register-service-worker";
// Only register service worker if explicitly enabled and in production // Add debug logging for environment variables
console.log('[ServiceWorker] Environment variables:', {
VITE_PWA_ENABLED: process.env.VITE_PWA_ENABLED,
NODE_ENV: process.env.NODE_ENV,
BASE_URL: process.env.BASE_URL,
CAN_REGISTER: process.env.VITE_PWA_ENABLED === "true" && process.env.NODE_ENV === "production"
});
// Modified condition to handle both string and boolean true
if ( if (
process.env.VITE_PWA_ENABLED === "true" && process.env.VITE_PWA_ENABLED === "true" &&
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
) { ) {
register(`${process.env.BASE_URL}sw.js`, { console.log('[ServiceWorker] Attempting to register service worker...');
// Use '/' as fallback if BASE_URL is undefined
const baseUrl = process.env.BASE_URL || '/';
register(`${baseUrl}sw.js`, {
ready() { ready() {
console.log("Service worker is active."); console.log("[ServiceWorker] Service worker is active.");
}, },
registered() { registered(registration) {
console.log("Service worker has been registered."); console.log("[ServiceWorker] Service worker has been registered:", registration);
}, },
cached() { cached(registration) {
console.log("Content has been cached for offline use."); console.log("[ServiceWorker] Content has been cached for offline use:", registration);
}, },
updatefound() { updatefound(registration) {
console.log("New content is downloading."); console.log("[ServiceWorker] New content is downloading:", registration);
}, },
updated() { updated(registration) {
console.log("New content is available; please refresh."); console.log("[ServiceWorker] New content is available:", registration);
}, },
offline() { offline() {
console.log( console.log("[ServiceWorker] No internet connection found. App is running in offline mode.");
"No internet connection found. App is running in offline mode.",
);
}, },
error(error) { error(error) {
console.error("Error during service worker registration:", error); console.error("[ServiceWorker] Error during service worker registration:", error);
}, },
}); });
} else { } else {
console.log( console.log(
"Service worker registration skipped - not enabled or not in production", "[ServiceWorker] Registration skipped:",
{
enabled: process.env.VITE_PWA_ENABLED === "true",
production: process.env.NODE_ENV === "production",
value: process.env.VITE_PWA_ENABLED,
type: typeof process.env.VITE_PWA_ENABLED
}
); );
} }
export function registerServiceWorker() {
// Skip service worker registration in Electron
if (window.electronAPI?.isElectron) {
console.log("Running in Electron - skipping service worker registration");
return;
}
// Regular service worker registration for web
if ("serviceWorker" in navigator) {
// ... existing code ...
}
}

View File

@@ -157,6 +157,11 @@ const routes: Array<RouteRecordRaw> = [
name: "InviteOneAcceptView", name: "InviteOneAcceptView",
component: () => import("../views/InviteOneAcceptView.vue"), component: () => import("../views/InviteOneAcceptView.vue"),
}, },
{
path: "/logs",
name: "logs",
component: () => import("../views/LogView.vue"),
},
{ {
path: "/new-activity", path: "/new-activity",
name: "new-activity", name: "new-activity",
@@ -281,6 +286,15 @@ const routes: Array<RouteRecordRaw> = [
name: "user-profile", name: "user-profile",
component: () => import("../views/UserProfileView.vue"), component: () => import("../views/UserProfileView.vue"),
}, },
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
]; ];
const isElectron = window.location.protocol === "file:"; const isElectron = window.location.protocol === "file:";

View File

@@ -29,7 +29,12 @@
*/ */
import { Router } from "vue-router"; import { Router } from "vue-router";
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks"; import {
deepLinkSchemas,
baseUrlSchema,
routeSchema,
DeepLinkRoute,
} from "../types/deepLinks";
import { logConsoleAndDb } from "../db"; import { logConsoleAndDb } from "../db";
import type { DeepLinkError } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks";
@@ -111,7 +116,7 @@ export class DeepLinkHandler {
): Promise<void> { ): Promise<void> {
const routeMap: Record<string, string> = { const routeMap: Record<string, string> = {
"user-profile": "user-profile", "user-profile": "user-profile",
project: "project", "project-details": "project-details",
"onboard-meeting-setup": "onboard-meeting-setup", "onboard-meeting-setup": "onboard-meeting-setup",
"invite-one-accept": "invite-one-accept", "invite-one-accept": "invite-one-accept",
"contact-import": "contact-import", "contact-import": "contact-import",
@@ -124,16 +129,37 @@ export class DeepLinkHandler {
did: "did", did: "did",
}; };
const routeName = routeMap[path]; // First try to validate the route path
if (!routeName) { let routeName: string;
try {
// Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = routeMap[validRoute];
} catch (error) {
// Log the invalid route attempt
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
// Redirect to error page with information about the invalid link
await this.router.replace({
name: "deep-link-error",
query: {
originalPath: path,
errorCode: "INVALID_ROUTE",
message: `The link you followed (${path}) is not supported`,
},
});
throw { throw {
code: "INVALID_ROUTE", code: "INVALID_ROUTE",
message: `Unsupported route: ${path}`, message: `Unsupported route: ${path}`,
}; };
} }
// Validate parameters based on route type // Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
try {
const validatedParams = await schema.parseAsync({ const validatedParams = await schema.parseAsync({
...params, ...params,
...query, ...query,
@@ -144,5 +170,22 @@ export class DeepLinkHandler {
params: validatedParams, params: validatedParams,
query, query,
}); });
} catch (error) {
// For parameter validation errors, provide specific error feedback
await this.router.replace({
name: "deep-link-error",
query: {
originalPath: path,
errorCode: "INVALID_PARAMETERS",
message: `The link parameters are invalid: ${(error as Error).message}`,
},
});
throw {
code: "INVALID_PARAMETERS",
message: (error as Error).message,
details: error,
};
}
} }
} }

View File

@@ -27,13 +27,35 @@
*/ */
import { z } from "zod"; import { z } from "zod";
// Base URL validation schema // Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = [
"user-profile",
"project-details",
"onboard-meeting-setup",
"invite-one-accept",
"contact-import",
"confirm-gift",
"claim",
"claim-cert",
"claim-add-raw",
"contact-edit",
"contacts",
"did",
] as const;
// Create a type from the array
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
// Update your schema definitions to use this type
export const baseUrlSchema = z.object({ export const baseUrlSchema = z.object({
scheme: z.literal("timesafari"), scheme: z.literal("timesafari"),
path: z.string(), path: z.string(),
queryParams: z.record(z.string()).optional(), queryParams: z.record(z.string()).optional(),
}); });
// Use the type to ensure route validation
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
// Parameter validation schemas for each route type // Parameter validation schemas for each route type
export const deepLinkSchemas = { export const deepLinkSchemas = {
"user-profile": z.object({ "user-profile": z.object({

25
src/types/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces";
export interface GiveRecordWithContactInfo extends GiveSummaryRecord {
jwtId: string;
fullClaim: GiveVerifiableCredential;
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;
description: string;
image?: string;
}

View File

@@ -1,18 +1,46 @@
import { logToDb } from "../db";
function safeStringify(obj: unknown) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
}
if (typeof value === "function") {
return `[Function: ${value.name || "anonymous"}]`;
}
return value;
});
}
export const logger = { export const logger = {
log: (message: string, ...args: unknown[]) => { log: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(message, ...args); console.log(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
} }
}, },
warn: (message: string, ...args: unknown[]) => { warn: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(message, ...args); console.warn(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
} }
}, },
error: (message: string, ...args: unknown[]) => { error: (message: string, ...args: unknown[]) => {
// Errors will always be logged
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(message, ...args); // Errors should always be logged console.error(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
}, },
}; };

View File

@@ -562,11 +562,22 @@
<router-link <router-link
id="switch-identity-link" id="switch-identity-link"
:to="{ name: 'identity-switcher' }" :to="{ name: 'identity-switcher' }"
class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2" class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
> >
Switch Identifier Switch Identifier
</router-link> </router-link>
<div class="flex mt-4">
<button>
<router-link
:to="{ name: 'statistics' }"
class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
>
See Global Animated History of Giving
</router-link>
</button>
</div>
<div id="sectionImportContactsSettings" class="mt-4"> <div id="sectionImportContactsSettings" class="mt-4">
<h2 class="text-slate-500 text-sm font-bold"> <h2 class="text-slate-500 text-sm font-bold">
Import Contacts & Settings Database Import Contacts & Settings Database
@@ -856,17 +867,6 @@
</div> </div>
</label> </label>
<div class="flex mt-4">
<button>
<router-link
:to="{ name: 'statistics' }"
class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
>
See Global Animated History of Giving
</router-link>
</button>
</div>
<div id="sectionPasskeyExpiration" class="flex justify-between"> <div id="sectionPasskeyExpiration" class="flex justify-between">
<span> <span>
<span class="text-slate-500 text-sm font-bold mb-2"> <span class="text-slate-500 text-sm font-bold mb-2">
@@ -912,6 +912,13 @@
/> />
</div> </div>
</label> </label>
<router-link
:to="{ name: 'logs' }"
class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
>
View Logs
</router-link>
</div> </div>
</section> </section>
</template> </template>
@@ -1071,7 +1078,7 @@ export default class AccountViewView extends Vue {
try { try {
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
const response = await this.axios.get( const response = await this.axios.get(
this.apiServer + this.partnerApiServer +
"/api/partner/userProfileForIssuer/" + "/api/partner/userProfileForIssuer/" +
this.activeDid, this.activeDid,
{ headers }, { headers },
@@ -1889,7 +1896,7 @@ export default class AccountViewView extends Vue {
); );
} }
const response = await this.axios.post( const response = await this.axios.post(
this.apiServer + "/api/partner/userProfile", this.partnerApiServer + "/api/partner/userProfile",
payload, payload,
{ headers }, { headers },
); );
@@ -1977,7 +1984,7 @@ export default class AccountViewView extends Vue {
try { try {
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
const response = await this.axios.delete( const response = await this.axios.delete(
this.apiServer + "/api/partner/userProfile", this.partnerApiServer + "/api/partner/userProfile",
{ headers }, { headers },
); );
if (response.status === 204) { if (response.status === 204) {

View File

@@ -829,7 +829,7 @@ export default class ConfirmGiftView extends Vue {
3000, 3000,
); );
} else { } else {
console.error("Got error submitting the confirmation:", result); logger.error("Got error submitting the confirmation:", result);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -5,9 +5,9 @@
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->
<div class="text-lg text-center font-light relative px-7"> <div class="relative px-7">
<h1 <h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()" @click="$router.back()"
> >
<font-awesome icon="chevron-left" class="fa-fw" /> <font-awesome icon="chevron-left" class="fa-fw" />

View File

@@ -0,0 +1,232 @@
<template>
<div class="deep-link-error">
<div class="safe-area-spacer"></div>
<h1>Invalid Deep Link</h1>
<div class="error-details">
<div class="error-message">
<h3>Error Details</h3>
<p>{{ errorMessage }}</p>
<div v-if="errorCode" class="error-code">
Error Code: <span>{{ errorCode }}</span>
</div>
</div>
<div v-if="originalPath" class="original-link">
<h3>Attempted Link</h3>
<code>timesafari://{{ formattedPath }}</code>
<div class="debug-info">
<h4>Parameters:</h4>
<pre>{{ JSON.stringify(route.params, null, 2) }}</pre>
<h4>Query:</h4>
<pre>{{ JSON.stringify(route.query, null, 2) }}</pre>
</div>
</div>
</div>
<div class="actions">
<button class="primary-button" @click="goHome">Go to Home</button>
<button class="secondary-button" @click="reportIssue">
Report Issue
</button>
</div>
<div class="supported-links">
<h2>Supported Deep Links</h2>
<ul>
<li v-for="(routeItem, index) in validRoutes" :key="index">
<code>timesafari://{{ routeItem }}/:id</code>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { VALID_DEEP_LINK_ROUTES } from "../types/deepLinks";
import { logConsoleAndDb } from "../db";
import { logger } from "../utils/logger";
const route = useRoute();
const router = useRouter();
// Extract error information from query params
const errorCode = computed(
() => (route.query.errorCode as string) || "UNKNOWN_ERROR",
);
const errorMessage = computed(
() =>
(route.query.message as string) ||
"The deep link you followed is invalid or not supported.",
);
const originalPath = computed(() => route.query.originalPath as string);
const validRoutes = VALID_DEEP_LINK_ROUTES;
// Format the path and include any parameters
const formattedPath = computed(() => {
if (!originalPath.value) return "";
const path = originalPath.value.replace(/^\/+/, "");
// Log for debugging
logger.log("Original Path:", originalPath.value);
logger.log("Route Params:", route.params);
logger.log("Route Query:", route.query);
return path;
});
// Navigation methods
const goHome = () => router.replace({ name: "home" });
const reportIssue = () => {
// Open a support form or email
window.open(
"mailto:support@timesafari.app?subject=Invalid Deep Link&body=" +
encodeURIComponent(
`I encountered an error with a deep link: timesafari://${originalPath.value}\nError: ${errorMessage.value}`,
),
);
};
// Log the error for analytics
onMounted(() => {
logConsoleAndDb(
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}`,
true,
);
});
</script>
<style scoped>
.deep-link-error {
padding-top: 60px;
padding-left: 20px;
padding-right: 20px;
max-width: 600px;
margin: 0 auto;
}
.safe-area-spacer {
height: env(safe-area-inset-top);
}
h1 {
color: #ff4444;
margin-bottom: 24px;
}
h2,
h3 {
color: #333;
margin-bottom: 12px;
}
.error-details {
background-color: #f8f8f8;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.error-message {
margin-bottom: 20px;
}
.error-message p {
color: #666;
line-height: 1.5;
margin-bottom: 12px;
}
.error-code {
font-family: monospace;
color: #666;
margin-top: 8px;
}
.error-code span {
background-color: #eee;
padding: 2px 6px;
border-radius: 4px;
}
.original-link {
padding: 12px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 6px;
}
.original-link code {
color: #0066cc;
font-family: monospace;
word-break: break-all;
}
.actions {
margin: 24px 0;
display: flex;
gap: 12px;
}
.actions button {
padding: 10px 20px;
border-radius: 6px;
border: none;
font-weight: 500;
cursor: pointer;
}
.primary-button {
background-color: #007aff;
color: white;
}
.secondary-button {
background-color: #f2f2f2;
color: #333;
}
.supported-links {
margin-top: 32px;
}
.supported-links ul {
list-style: none;
padding: 0;
}
.supported-links li {
padding: 8px 12px;
background-color: #f8f8f8;
border-radius: 4px;
margin-bottom: 8px;
}
.supported-links code {
font-family: monospace;
color: #0066cc;
}
.debug-info {
margin-top: 16px;
padding: 12px;
background-color: #f0f0f0;
border-radius: 4px;
}
.debug-info h4 {
margin: 8px 0;
color: #666;
font-size: 14px;
}
.debug-info pre {
white-space: pre-wrap;
word-break: break-all;
font-family: monospace;
font-size: 12px;
color: #333;
background-color: #fff;
padding: 8px;
border-radius: 4px;
margin: 4px 0;
}
</style>

View File

@@ -187,23 +187,23 @@
</div> </div>
<!-- Results List --> <!-- Results List -->
<div class="bg-slate-100 rounded-md px-4 py-3 mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<h2 class="text-xl font-bold"> <h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity Latest Activity
<button @click="openFeedFilters()"> <button
<span class="text-xs text-white">
<font-awesome
v-if="resultsAreFiltered()" v-if="resultsAreFiltered()"
icon="filter" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md" @click="openFeedFilters()"
/> >
<font-awesome <font-awesome icon="filter" class="fa-fw" />
</button>
<button
v-else v-else
icon="filter" class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md" @click="openFeedFilters()"
/> >
</span> <font-awesome icon="filter" class="fa-fw" />
</button> </button>
</h2> </h2>
</div> </div>
@@ -250,84 +250,20 @@
</div> </div>
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="border-t border-slate-300"> <ul id="listLatestActivity" class="space-y-4">
<li <ActivityListItem
v-for="record in feedData" v-for="record in feedData"
:key="record.jwtId" :key="record.jwtId"
class="border-b border-slate-300 py-2" :record="record"
> :last-viewed-claim-id="feedLastViewedClaimId"
<div :is-registered="isRegistered"
v-if="record.jwtId == feedLastViewedClaimId" :active-did="activeDid"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm" :confirmer-id-list="record.confirmerIdList"
> @load-claim="onClickLoadClaim"
You've already seen all the following @view-image="openImageViewer"
</div> @cache-image="cacheImageData"
@confirm-claim="confirmClaim"
<div class="grid grid-cols-12">
<span class="pt-1 col-span-1 justify-self-start">
<span>
<font-awesome
icon="circle-user"
:class="
computeKnownPersonIconStyleClassNames(
record.giver.known || record.receiver.known,
)
"
@click="toastUser('This involves your contacts.')"
/> />
<font-awesome
icon="gift"
class="pl-3 text-slate-500"
@click="toastUser('This is a gift.')"
/>
</span>
</span>
<span class="col-span-10 justify-self-stretch overflow-hidden">
<span class="pl-2 block break-words">
{{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)">
<font-awesome
icon="file-lines"
class="pl-2 text-slate-500 cursor-pointer"
/>
</a>
</span>
<span class="col-span-1 justify-self-end">
<router-link
v-if="record.fulfillsPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.fulfillsPlanHandleId)
"
>
<font-awesome icon="hammer" class="text-blue-500" />
</router-link>
<router-link
v-if="record.providerPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.providerPlanHandleId)
"
>
<font-awesome icon="hammer" class="text-blue-500" />
</router-link>
</span>
</div>
<div v-if="record.image" class="w-full">
<div
class="cursor-pointer"
@click="openImageViewer(record.image)"
>
<img
:src="record.image"
class="w-full aspect-[3/2] object-cover rounded-xl mt-2"
alt="shared content"
@load="cacheImageData($event, record.image)"
/>
</div>
</div>
</li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
<div v-if="isFeedLoading"> <div v-if="isFeedLoading">
@@ -369,6 +305,7 @@ import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue"; import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
import ImageViewer from "../components/ImageViewer.vue"; import ImageViewer from "../components/ImageViewer.vue";
import ActivityListItem from "../components/ActivityListItem.vue";
import { import {
AppString, AppString,
NotificationIface, NotificationIface,
@@ -403,23 +340,10 @@ import {
OnboardPage, OnboardPage,
} from "../libs/util"; } from "../libs/util";
import { GiveSummaryRecord } from "../interfaces"; import { GiveSummaryRecord } from "../interfaces";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { import * as serverUtil from "../libs/endorserServer";
jwtId: string;
giver: {
displayName: string;
known: boolean;
profileImageUrl?: string;
};
image?: string;
providerPlanName?: string;
recipientProjectName?: string;
receiver: {
displayName: string;
known: boolean;
profileImageUrl?: string;
};
}
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "types";
/** /**
* HomeView - Main view component for the application's home page * HomeView - Main view component for the application's home page
* *
@@ -442,6 +366,7 @@ import { logger } from "../utils/logger";
TopMessage, TopMessage,
UserNameDialog, UserNameDialog,
ImageViewer, ImageViewer,
ActivityListItem,
}, },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
@@ -522,10 +447,88 @@ export default class HomeView extends Vue {
this.isCreatingIdentifier = false; this.isCreatingIdentifier = false;
this.allMyDids = [newDid]; this.allMyDids = [newDid];
} }
} catch (error) {
logConsoleAndDb( const settings = await retrieveSettingsForActiveAccount();
"Error retrieving all account DIDs on home page:" + error, this.apiServer = settings.apiServer || "";
true, this.activeDid = settings.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings.lastViewedClaimId;
this.givenName = settings.firstName || "";
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isRegistered = !!settings.isRegistered;
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home,
);
}
// someone may have have registered after sharing contact info, so recheck
if (!this.isRegistered && this.activeDid) {
try {
const resp = await fetchEndorserRateLimits(
this.apiServer,
this.axios,
this.activeDid,
);
if (resp.status === 200) {
await updateAccountSettings(this.activeDid, {
isRegistered: true,
...(await retrieveSettingsForActiveAccount()),
});
this.isRegistered = true;
}
} catch (e) {
// ignore the error... just keep us unregistered
}
}
// this returns a Promise but we don't need to wait for it
this.updateAllFeed();
if (this.activeDid) {
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
);
this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
}
if (this.activeDid) {
const offersToUserProjects = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
);
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
err.userMessage ||
"There was an error retrieving your settings or the latest activity.",
},
5000,
); );
} }
} }
@@ -802,6 +805,12 @@ export default class HomeView extends Vue {
this.allMyDids, this.allMyDids,
), ),
image: claim.image, image: claim.image,
issuer: didInfoForContact(
record.issuerDid,
this.activeDid,
contactForDid(record.issuerDid, this.allContacts),
this.allMyDids,
),
providerPlanHandleId: provider?.identifier as string, providerPlanHandleId: provider?.identifier as string,
providerPlanName: providedByPlan?.name as string, providerPlanName: providedByPlan?.name as string,
recipientProjectName: fulfillsPlan?.name as string, recipientProjectName: fulfillsPlan?.name as string,
@@ -1079,5 +1088,67 @@ export default class HomeView extends Vue {
this.selectedImage = imageUrl; this.selectedImage = imageUrl;
this.isImageViewerOpen = true; this.isImageViewerOpen = true;
} }
async confirmClaim(record: GiveRecordWithContactInfo) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Confirm",
text: "Do you personally confirm that this is true?",
onYes: async () => {
const goodClaim = serverUtil.removeSchemaContext(
serverUtil.removeVisibleToDids(
serverUtil.addLastClaimOrHandleAsIdIfMissing(
record.fullClaim,
record.jwtId,
record.handleId,
),
),
);
const confirmationClaim = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
3000,
);
// Refresh the feed to show updated confirmation status
await this.updateAllFeed();
} else {
logger.error("Error submitting confirmation:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
},
5000,
);
}
},
},
-1,
);
}
} }
</script> </script>

98
src/views/LogView.vue Normal file
View File

@@ -0,0 +1,98 @@
<!-- This is useful in an environment where the download doesn't work. -->
<template>
<QuickNav selected="" />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back Button -->
<div class="relative px-7">
<h1
class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="mr-2" />
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-6">Logs</h1>
<!-- Error Message -->
<div
v-if="error"
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"
>
<span>{{ error }}</span>
</div>
<!-- Log Content -->
<div v-if="loading" class="text-center">
<font-awesome icon="spinner" class="fa-spin text-slate-400" />
Loading logs...
</div>
<div v-else-if="!logs.length" class="text-center text-slate-500">
No logs found.
</div>
<div v-else>
<div v-for="(log, index) in logs" :key="index" class="mb-8">
<h2 class="text-lg font-semibold mb-2">{{ log.date }}</h2>
<pre
class="bg-slate-100 p-4 rounded-md overflow-x-auto whitespace-pre-wrap"
>{{ log.message }}</pre
>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { db } from "../db/index";
import { Log } from "../db/tables/logs";
import { logger } from "../utils/logger";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class LogView extends Vue {
$router!: Router;
loading = true;
logs: Log[] = [];
error: string | null = null;
async mounted() {
await this.loadLogs();
}
async loadLogs() {
try {
this.error = null; // Clear any previous errors
await db.open();
// Get all logs and sort by date in reverse chronological order
const allLogs = await db.logs.toArray();
this.logs = allLogs.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB.getTime() - dateA.getTime();
});
} catch (error) {
logger.error("Error loading logs:", error);
this.error =
error instanceof Error
? error.message
: `An unknown error occurred while loading logs: ${String(error)}`;
} finally {
this.loading = false;
}
}
}
</script>

View File

@@ -372,7 +372,10 @@
<!-- Totals section --> <!-- Totals section -->
<div class="mt-1 flex items-center min-h-[1.5rem]"> <div class="mt-1 flex items-center min-h-[1.5rem]">
<div v-if="loadingTotals" class="flex-1"> <div v-if="loadingTotals" class="flex-1">
<fa icon="spinner" class="fa-spin-pulse text-blue-500" /> <font-awesome
icon="spinner"
class="fa-spin-pulse text-blue-500"
/>
</div> </div>
<div v-else-if="givesTotalsByUnit.length > 0" class="flex-1"> <div v-else-if="givesTotalsByUnit.length > 0" class="flex-1">
<span class="font-semibold mr-2 shrink-0">Totals</span> <span class="font-semibold mr-2 shrink-0">Totals</span>
@@ -391,7 +394,7 @@
</span> </span>
<span v-if="givesTotalsByUnit.length > 1">...</span> <span v-if="givesTotalsByUnit.length > 1">...</span>
<span> <span>
<fa <font-awesome
:icon="totalsExpanded ? 'chevron-up' : 'chevron-right'" :icon="totalsExpanded ? 'chevron-up' : 'chevron-right'"
class="fa-fw text-xs ml-1" class="fa-fw text-xs ml-1"
/> />
@@ -404,7 +407,7 @@
:key="total.unit" :key="total.unit"
class="ml-2" class="ml-2"
> >
<fa <font-awesome
:icon="libsUtil.iconForUnitCode(total.unit)" :icon="libsUtil.iconForUnitCode(total.unit)"
class="fa-fw text-slate-400 mr-1" class="fa-fw text-slate-400 mr-1"
/> />
@@ -431,7 +434,7 @@
> >
<div class="flex justify-between gap-4"> <div class="flex justify-between gap-4">
<span> <span>
<fa icon="user" class="fa-fw text-slate-400" /> <font-awesome icon="user" class="fa-fw text-slate-400" />
{{ {{
serverUtil.didInfo( serverUtil.didInfo(
give.agentDid, give.agentDid,
@@ -442,23 +445,26 @@
}} }}
</span> </span>
<span v-if="give.amount" class="whitespace-nowrap"> <span v-if="give.amount" class="whitespace-nowrap">
<fa <font-awesome
:icon="libsUtil.iconForUnitCode(give.unit)" :icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
/>{{ give.amount }} />{{ give.amount }}
</span> </span>
</div> </div>
<div class="text-slate-500"> <div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" /> <font-awesome icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }} {{ give.issuedAt?.substring(0, 10) }}
</div> </div>
<div v-if="give.description" class="text-slate-500"> <div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" /> <font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ give.description }} {{ give.description }}
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)"> <a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" /> <font-awesome
icon="file-lines"
class="text-blue-500 cursor-pointer"
/>
</a> </a>
<a <a
@@ -468,16 +474,16 @@
" "
@click="deepCheckConfirmable(give)" @click="deepCheckConfirmable(give)"
> >
<fa <font-awesome
icon="circle-check" icon="circle-check"
class="text-blue-500 cursor-pointer" class="text-blue-500 cursor-pointer"
/> />
</a> </a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId"> <a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<fa icon="spinner" class="fa-spin-pulse" /> <font-awesome icon="spinner" class="fa-spin-pulse" />
</a> </a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)"> <a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<fa <font-awesome
icon="circle-check" icon="circle-check"
class="text-slate-500 cursor-pointer" class="text-slate-500 cursor-pointer"
/> />

View File

@@ -1,123 +0,0 @@
#!/bin/bash
# Configurable pause duration (in seconds)
PAUSE_DURATION=2
MANUAL_CONTINUE=true
# Function to test deep link
test_link() {
echo "----------------------------------------"
echo "Testing: $1"
echo "Description: $2"
echo "----------------------------------------"
adb shell am start -W -a android.intent.action.VIEW -d "$1" app.timesafari.app
if [ "$MANUAL_CONTINUE" = true ]; then
read -p "Press Enter to continue to next test..."
else
sleep $PAUSE_DURATION
fi
}
# Allow command line override of pause settings
while getopts "t:a" opt; do
case $opt in
t) PAUSE_DURATION=$OPTARG ;;
a) MANUAL_CONTINUE=false ;;
esac
done
echo "Starting TimeSafari Deep Link Tests"
echo "======================================"
echo "Pause duration: $PAUSE_DURATION seconds"
echo "Manual continue: $MANUAL_CONTINUE"
# Contact Import Routes
echo "\nTesting Contact Import Routes:"
# 1. Direct Query Parameter Import (URL-encoded JSON)
QUERY_CONTACTS='[{
"did":"did:ethr:"
},{
"did":"did:ethr:",
"name":"Jordan",
"nextPubKeyHashB64":"IBfRZfwdzeKOzqCx8b+WlLpMJHOAT9ZknIDJo7F3rZE=",
"publicKeyBase64":"A1eIndfaxgMpVwyD5dYe74DgjuIo5SwPZFCcLdOemjf"
}]'
ENCODED_CONTACTS=$(echo $QUERY_CONTACTS | jq -c | python3 -c "import urllib.parse; print(urllib.parse.quote(input()))")
test_link "timesafari://contact-import?contacts=$ENCODED_CONTACTS" "Bulk import via query parameters"
# 2. JWT Path Imports
# Original JWT with multiple contacts
BULK_JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDA3NDA0NTMsImNvbnRhY3RzIjpbeyJkaWQiOiJkaWQ6ZXRocjoweGY5NjlBNURlRTdhNTgwNmQxQzM3ZjRFYzQ5QTU1NUFiOTc5MTEwODkifSx7ImRpZCI6ImRpZDpldGhyOjB4RkVkM2I0MTY5NDZiMjNGM0Y0NzI3OTkwNTMxNDRCNEUzNDE1NUI1YiIsIm5hbWUiOiJKb3JkYW4iLCJuZXh0UHViS2V5SGFzaEI2NCI6IklCZlJaZndkemVLT3pxQ3g4YitXbExwTUpIT0FUOVprbklESm83RjNyWkU9IiwicHVibGljS2V5QmFzZTY0IjoiQTFlSW5kZmF4Z01wVnd5RDVkWWU3NERnanQ5SW81U3dQWkZDY0xkT2VtamYifV0sImlzcyI6ImRpZDpldGhyOjB4RDUzMTE0ODMwRDRhNUQ5MDQxNkI0M0ZjOTlhMjViMGRGOGJiMUJBZCJ9.yKEFounxUGU9-grAMFHA12dif9BKYkftg8F3wAIcFYh0H_k1tevjEYyD1fvAyIxYxK5xR0E8moqMhi78ipJXcg"
test_link "timesafari://contact-import/$BULK_JWT" "Multiple contacts via JWT"
# 3. Contact Page JWT Redirect
test_link "timesafari://contacts?contactJwt=$BULK_JWT" "Multiple contacts redirect"
# Contact Management Routes
test_link "timesafari://contact-edit/did:ethr:" \
"Edit first contact"
# Error Cases
echo "\nTesting Contact Import Error Cases:"
test_link "timesafari://contact-import/eyJJTlZBTElEIn0" "Invalid JWT format"
test_link "timesafari://contact-import?contacts=[{invalid:data}]" "Invalid contact data"
# Original Routes (preserved from previous version)
echo "\nTesting Other Routes:"
# Test claim routes
echo "\nTesting Claim Routes:"
test_link "timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H"
test_link "timesafari://claim-cert/01JMAAFZRNSRTQ0EBSD70A8E1H"
# Test project routes
echo "\nTesting Project Routes:"
test_link "timesafari://project/https%3A%2F%2Fendorser.ch%2Fentity%2F01JKW0QZX1XVCVZV85VXAMB31R"
# Test gift routes
echo "\nTesting Gift Routes:"
test_link "timesafari://confirm-gift/01JMTC8T961KFPP2N8ZB92ER4K"
# Test offer routes
echo "\nTesting Offer Routes:"
test_link "timesafari://offer-details/101"
# Test complex query parameters
echo "\nTesting Complex Query Parameters:"
test_link "timesafari://contact-import/jwt?contacts=%5B%7B%22name%22%3A%22Test%22%7D%5D"
# New test cases
echo "\nTesting DID Routes:"
test_link "timesafari://did/did:example:123"
test_link "timesafari://did/did:example:456?view=details"
echo "\nTesting Additional Contact Routes:"
test_link "timesafari://contact-import/jwt?contacts=%5B%7B%22did%22%3A%22did%3Aexample%3A123%22%7D%5D"
test_link "timesafari://contact-edit/did:example:123?action=edit"
echo "\nTesting Error Cases:"
test_link "timesafari://invalid-route/123"
test_link "timesafari://claim/123?view=invalid"
test_link "timesafari://did/invalid-did"
# Single invite JWT test
# Header: {"typ":"JWT","alg":"ES256K"}
# Payload: {
# "iat": 1740740453,
# "contact": {
# "did": "did:ethr:",
# "name": "Jordan",
# "nextPubKeyHashB64": "IBfRZfwdzeKOzqCx8b+WlLpMJHOAT9ZknIDJo7F3rZE=",
# "publicKeyBase64": "A1eIndfaxgMpVwyD5dYe74DgjuIo5SwPZFCcLdOemjf"
# },
# "iss": "did:ethr:"
# }
SINGLE_INVITE_JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDA3NDA0NTMsImNvbnRhY3QiOnsiZGlkIjoiZGlkOmV0aHI6MHhGRWQzYjQxNjk0NmIyM0YzRjQ3Mjc5OTA1MzE0NEI0RTM0MTU1QjViIiwibmFtZSI6IkpvcmRhbiIsIm5leHRQdWJLZXlIYXNoQjY0IjoiSUJmUlpmd2R6ZUtPenFDeDhiK1dsTHBNSkhPQVQ5WmtuSURKbzdGM3JaRT0iLCJwdWJsaWNLZXlCYXNlNjQiOiJBMWVJbmRmYXhnTXBWd3lENWRZZTc0RGdqdUlvNVN3UFpGQ2NMZEtlbWpmIn0sImlzcyI6ImRpZDpldGhyOjB4RDUzMTE0ODMwRDRhNUQ5MDQxNkI0M0ZjOTlhMjViMGRGOGJiMUJBZCJ9.yKEFounxUGU9-grAMFHA12dif9BKYkftg8F3wAIcFYh0H_k1tevjEYyD1fvAyIxYxK5xR0E8moqMhi78ipJXcg"
test_link "timesafari://invite-one-accept/$SINGLE_INVITE_JWT" "Single contact invite via JWT"
echo "\nDeep link testing complete"
echo "======================================"

View File

@@ -27,10 +27,16 @@ npx playwright install
#### Full Test Suite #### Full Test Suite
Run all local tests: To run all tests, make sure XCode is started and either Android Studio is started or an Android device is connected.
```bash ```bash
npm run test-all npm run test:all
```
Run only web tests:
```bash
npm run test:web
``` ```
Note: Tests may occasionally fail and succeed on rerun (especially if a different test fails). Note: Tests may occasionally fail and succeed on rerun (especially if a different test fails).

View File

@@ -9,3 +9,4 @@ eth-utils>=2.1.0 # For Ethereum utilities
pyjwt>=2.8.0 # For JWT operations pyjwt>=2.8.0 # For JWT operations
cryptography>=42.0.0 # For key format conversion cryptography>=42.0.0 # For key format conversion
jwcrypto jwcrypto
setuptools

View File

@@ -16,11 +16,14 @@ export async function createBuildConfig(mode: string) {
const isElectron = mode === "electron"; const isElectron = mode === "electron";
const isCapacitor = mode === "capacitor"; const isCapacitor = mode === "capacitor";
const isPyWebView = mode === "pywebview"; const isPyWebView = mode === "pywebview";
const isWeb = mode === "web";
// Explicitly set platform // Explicitly set platform
process.env.VITE_PLATFORM = mode; process.env.VITE_PLATFORM = mode;
if (isElectron || isPyWebView || isCapacitor) { if (isWeb) {
process.env.VITE_PWA_ENABLED = 'true';
} else {
process.env.VITE_PWA_ENABLED = 'false'; process.env.VITE_PWA_ENABLED = 'false';
} }
@@ -42,7 +45,7 @@ export async function createBuildConfig(mode: string) {
define: { define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.VITE_PLATFORM': JSON.stringify(mode), 'process.env.VITE_PLATFORM': JSON.stringify(mode),
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)), 'process.env.VITE_PWA_ENABLED': isWeb ? "true" : "false",
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""', __dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
}, },
resolve: { resolve: {

View File

@@ -24,6 +24,25 @@ export default defineConfig(async () => {
}; };
} }
} }
}] }],
build: {
outDir: 'dist-electron',
emptyOutDir: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
},
assetsDir: 'assets',
minify: 'terser',
terserOptions: {
compress: {
drop_console: false,
},
},
},
base: './',
}); });
}); });