Compare commits

...

31 Commits

Author SHA1 Message Date
Matthew Raymer
cd32c37cfb fix: standardize app package name to app.timesafari.app
Update package name consistently across all configurations to fix deep linking and app identification issues.

Changes:
- Update appId in package.json build config from "app.timesafari" to "app.timesafari.app"
- Update force-stop commands in test scripts to use correct package name
- Update URL scheme registration in iOS Info.plist to use consistent bundle ID
- Update app identifier fallback values in iOS test script
- Update serve script to use production mode with host flag

Technical Details:
- Standardized package name to "app.timesafari.app" across all configurations
- Fixed ADB commands in test scripts to use correct package identifier
- Updated iOS URL scheme registration to match Android package name
- Added proper NODE_ENV and host configuration for serve command

Benefits:
- Fixes deep linking functionality by using consistent package identifiers
- Ensures correct app identification across Android and iOS platforms
- Improves test reliability with correct package references
- Enables proper cross-platform URL scheme handling
2025-03-25 08:53:24 +00: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
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
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
60 changed files with 5136 additions and 1740 deletions

View File

@@ -4,6 +4,8 @@ This guide explains how to build TimeSafari for different platforms.
## Prerequisites
For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended)
- npm (comes with Node.js)
- Git
@@ -11,6 +13,17 @@ This guide explains how to build TimeSafari for different platforms.
- For Android builds: Android Studio with SDK installed
- 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
1. Clone the repository:
@@ -114,6 +127,13 @@ Prerequisites: macOS with Xcode installed
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:
```bash
@@ -122,16 +142,6 @@ Prerequisites: macOS with Xcode installed
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
Prerequisites: Android Studio with SDK installed
@@ -150,22 +160,19 @@ Prerequisites: Android Studio with SDK installed
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
npx cap open android
```
3. 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.
5. Use Android Studio to build and run on emulator or device.
## 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
```
... or, to create the `aab` file, `bundle` instead of `build`:
```bash
./gradlew bundle -Dlint.baselines.continue=true
```
## Configuring Android for deep links
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
Run local tests:
Run all tests (requires XCode and Android Studio/device):
```bash
npm run test-local
npm run test:all
```
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 Android: Correct SDK version must be installed
- 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.app
# Clear app data (if you don't want to fully uninstall)
adb shell pm clear app.timesafari.app
# Uninstall for all users
adb shell pm uninstall -k --user 0 app.timesafari.app
# Check if app is installed
adb shell pm path app.timesafari.app

View File

@@ -1,2 +1,2 @@
#Tue Mar 11 10:01:05 UTC 2025
gradle.version=8.10.2
#Fri Mar 21 07:27:50 UTC 2025
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/.npmkeep
src/main/assets/public/assets/

View File

@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
android {
namespace "app.timesafari"
namespace 'app.timesafari'
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "app.timesafari"
@@ -22,17 +22,6 @@ android {
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 {

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;
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;
package com.getcapacitor.myapp;
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)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("app.timesafari", appContext.getPackageName());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,9 +7,8 @@ buildscript {
mavenCentral()
}
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 "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -28,10 +27,3 @@ allprojects {
task clean(type: Delete) {
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
android.useAndroidX=true
android.suppressUnsupportedCompileSdk=34
android.suppressUnsupportedCompileSdk=34
android.suppressUnsupportedCompileSdk=34

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
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 = {
appId: 'app.timesafari.app',
appId: 'app.timesafari',
appName: 'TimeSafari',
webDir: 'dist',
bundledWebRuntime: false,

3824
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,12 @@
},
"scripts": {
"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",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore 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",
"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: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",
@@ -118,6 +117,7 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2",
"@types/dom-webcodecs": "^0.1.7",
"@types/js-yaml": "^4.0.9",
@@ -155,7 +155,7 @@
},
"main": "./dist-electron/main.js",
"build": {
"appId": "app.timesafari",
"appId": "app.timesafari.app",
"productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages"

View File

@@ -30,6 +30,7 @@
*
* @requires child_process
* @requires path
* @requires readline
*
* @author TimeSafari Team
* @license MIT
@@ -37,7 +38,14 @@
const { execSync } = require('child_process');
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
const getLogFileName = () => {
@@ -88,11 +96,57 @@ const verifyJavaInstallation = (log) => {
// Generate test data using generate_data.ts
const generateTestData = async (log) => {
log('🔄 Generating test data...');
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
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');
} catch (error) {
throw new Error(`Failed to generate test data: ${error.message}`);
log(`Failed to generate test data: ${error.message}`);
throw error;
}
};
@@ -122,8 +176,18 @@ const executeDeeplink = async (url, description, log) => {
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
log(`✅ Successfully executed: ${description}`);
// Wait between deeplink tests
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s
// Wait for app to load content
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) {
log(`❌ Failed to execute deeplink: ${description}`);
log(`Error: ${error.message}`);
@@ -137,16 +201,12 @@ const runDeeplinkTests = async (log) => {
try {
// 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 contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
// Test each deeplink
// Test URLs
const deeplinkTests = [
{
url: `timesafari://claim/${claimDetails.claim_id}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${claimDetails.claim_id}`,
description: 'Claim certificate view'
@@ -173,26 +233,44 @@ const runDeeplinkTests = async (log) => {
}
];
// Execute each test
for (const test of deeplinkTests) {
await executeDeeplink(test.url, test.description, log);
}
// Show test plan
log('\n📋 Test Plan:');
deeplinkTests.forEach((test, i) => {
log(`${i + 1}. ${test.description}`);
});
let succeeded = true;
try {
await executeDeeplink('timesafari://contactJunk', 'Non-existent deeplink', log);
} catch (error) {
log('✅ Non-existent deeplink failed as expected');
succeeded = false;
} finally {
if (succeeded) {
throw new Error('Non-existent deeplink should have failed');
// Execute each test
let testsCompleted = 0;
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);
testsCompleted++;
// 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('------------------------');
}
}
log(' All deeplink tests completed successfully');
log('\n🎉 All deeplink tests completed successfully!');
rl.close(); // Close readline interface when done
} catch (error) {
log('❌ Deeplink tests failed');
rl.close(); // Close readline interface on error
throw error;
}
};
@@ -214,24 +292,88 @@ const configureAndroidProject = async (log) => {
log('⚙️ Configuring 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');
log('✅ Added SDK suppression to gradle.properties');
} else {
log('✅ SDK suppression already configured in gradle.properties');
}
};
// Build and test Android project
const buildAndTestAndroid = async (log, env) => {
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 });
log('✅ Gradle clean completed');
// Build
log('🏗️ Building project...');
execSync('cd android && ./gradlew build', { stdio: 'inherit', env });
log('✅ Gradle build completed');
// Run tests with retry
log('🧪 Running Android tests...');
execSync('cd android && ./gradlew connectedAndroidTest', { stdio: 'inherit', env });
log('✅ Android tests completed');
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');
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
@@ -296,4 +438,10 @@ async function runAndroidTests() {
}
// 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.
*
* Process flow:
* 1. Sync Capacitor project with latest web build
* 2. Build app for iOS simulator
* 3. Run XCTest suite
* 1. Clean and reset iOS platform (if needed)
* 2. Check prerequisites (Xcode, CocoaPods, Capacitor setup)
* 3. Sync Capacitor project with latest web build
* 4. Build app for iOS simulator
* 5. Run XCTest suite
*
* Prerequisites:
* - macOS operating system
@@ -38,7 +40,21 @@
const { execSync } = require('child_process');
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
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
const checkSimulator = async (log) => {
log('🔍 Checking for iOS simulator...');
@@ -151,122 +330,84 @@ const verifyXcodeInstallation = (log) => {
// Generate test data using generate_data.ts
const generateTestData = async (log) => {
log('🔄 Generating test data...');
log('\n🔍 DEBUG: Starting test data generation...');
// Check if test-scripts directory exists
if (!existsSync('test-scripts')) {
log('⚠️ test-scripts directory not found');
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}`);
}
}
}
// Check directory structure
log('📁 Current directory:', process.cwd());
log('📁 Directory contents:', require('fs').readdirSync('.'));
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
log('📁 Creating .generated directory');
mkdirSync('.generated', { recursive: true });
}
try {
// Try to generate test data using the script
log('🔄 Running test data generation script...');
log('🔄 Attempting to run generate_data.ts...');
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 = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying generated files:');
log('\n📝 Verifying generated files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} was not generated`);
throw new Error(`Required file ${file} was not generated`);
log(`❌ Missing file: ${file}`);
} 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) {
log(`⚠️ Failed to generate test data: ${error.message}`);
log(`\n⚠️ Test data generation failed: ${error.message}`);
log('⚠️ Creating fallback test data...');
// Create minimal fallback test data
// Create fallback data with detailed logging
const fallbackTestEnv = {
"CONTACT1_DID": "did:example:123456789",
"CONTACT1_DID": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B",
"APP_URL": "https://app.timesafari.example"
};
const fallbackClaimDetails = {
"claim_id": "claim_12345",
"title": "Test Claim",
"description": "This is a test claim"
};
const fallbackContacts = [
{
"id": "contact1",
"name": "Test Contact",
"did": "did:example:123456789"
"did": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B"
}
];
// Use writeFileSync to overwrite any existing files
const { writeFileSync } = require('fs');
log('\n📝 Writing fallback data:');
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/claim_details.json', JSON.stringify(fallbackClaimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2));
log('✅ Fallback test data created');
// Verify files were created
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying fallback files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Failed to create ${file}`);
} else {
log(`✅ Created ${file}`);
}
// Verify fallback data was written
log('\n🔍 Verifying fallback data:');
try {
const writtenTestEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
const writtenContacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
log('Written TestEnv:', writtenTestEnv);
log('Written Contacts:', writtenContacts);
} catch (e) {
log('❌ Error verifying fallback data:', e);
}
}
};
@@ -282,40 +423,45 @@ const buildWebAssets = async (log) => {
// Configure iOS project
const configureIosProject = async (log) => {
log('📱 Syncing Capacitor project...');
try {
execSync('npx cap sync ios', { stdio: 'inherit' });
log('✅ Capacitor sync completed');
} catch (error) {
log('⚠️ Capacitor sync encountered issues. Attempting to continue...');
}
log('📱 Configuring iOS project...');
// Skip cap sync since we just did a clean platform add
log('✅ Using freshly created iOS platform');
// Register URL scheme for deeplink tests
log('🔗 Configuring URL scheme for deeplink tests...');
if (checkAndRegisterUrlScheme(log)) {
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...');
try {
// Try to run pod install normally first
log('🔄 Running "pod install" in ios/App directory...');
execSync('cd ios/App && pod install', { stdio: 'inherit' });
log('✅ CocoaPods installation completed');
} 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.');
}
// 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...');
}
log('✅ CocoaPods installation completed');
// 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
@@ -365,6 +511,96 @@ const runIosApp = async (log, simulator) => {
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
* Optionally tests deeplinks if the test data is available
@@ -373,122 +609,115 @@ const runIosApp = async (log, simulator) => {
* @returns {Promise<void>}
*/
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 {
({ testEnv, contacts } = validateTestData(log));
} catch (error) {
log('❌ Cannot proceed with tests due to invalid test data');
log(`Error: ${error.message}`);
log('Please ensure test data is properly generated before running tests');
process.exit(1); // Exit with error code
}
// Now we can safely create the deeplink tests knowing we have valid data
const deeplinkTests = [
{
url: `timesafari://claim/${testEnv.CLAIM_ID}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${testEnv.CERT_ID || testEnv.CLAIM_ID}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${testEnv.RAW_CLAIM_ID || testEnv.CLAIM_ID}`,
description: 'Raw claim addition'
},
{
url: 'timesafari://did/test',
description: 'DID view with test identifier'
},
{
url: `timesafari://did/${testEnv.CONTACT1_DID}`,
description: 'DID view with contact 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'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
];
// 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 {
// Load test data
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) {
log(`⚠️ Failed to load test-env.json: ${error.message}`);
return;
}
try {
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 = [
{
url: `timesafari://claim/${claimDetails.claim_id}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${claimDetails.claim_id}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${claimDetails.claim_id}`,
description: 'Raw claim addition'
},
{
url: 'timesafari://did/test',
description: 'DID view with test identifier'
},
{
url: `timesafari://did/${testEnv.CONTACT1_DID}`,
description: 'DID view with contact DID'
},
{
url: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`,
description: 'Contact editing'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
];
// Execute each test
let testsCompleted = 0;
let testsSkipped = 0;
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 {
log(`\n🔗 Testing deeplink: ${test.description}`);
log(`URL: ${test.url}`);
log('🚀 Executing deeplink test...');
log('⚠️ iOS SECURITY DIALOG WILL APPEAR - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' });
log(`✅ Successfully executed: ${test.description}`);
testsCompleted++;
// Wait between tests
await new Promise(resolve => setTimeout(resolve, 5000));
// Show progress
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) {
const errorMessage = deeplinkError.message || '';
@@ -501,22 +730,35 @@ const runDeeplinkTests = async (log) => {
log(`⚠️ Error: ${errorMessage}`);
}
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) {
log('\n📝 Note about skipped tests:');
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('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) {
log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing');
// Don't rethrow the error to prevent halting the process
}
};
@@ -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.app';
} catch (error) {
console.error('Error getting app identifier:', error);
return 'app.timesafari.app'; // Default fallback
}
};
/**
* Runs the complete iOS test suite including build and testing
*
* The function performs the following steps:
* 1. Syncs the Capacitor project with latest web build
* 2. Builds the app using xcodebuild
* 3. Optionally runs tests if configured
* 4. Launches the app in the simulator
* 1. Cleans and resets the iOS platform
* 2. Verifies prerequisites and project setup
* 3. Syncs the Capacitor project with latest web build
* 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.
*
@@ -617,7 +896,16 @@ async function runIosTests() {
try {
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);
// Verify Xcode installation
@@ -626,8 +914,7 @@ async function runIosTests() {
// Check for simulator or boot one if needed
const simulator = await checkSimulator(log);
// Build web assets and configure iOS project
await buildWebAssets(log);
// Configure iOS project
await configureIosProject(log);
// 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 size-[3rem] sm:size-[4rem] object-cover"
/>
</div>
<div v-else>
<font-awesome icon="person-circle-question" class="text-slate-300 text-[3rem] sm:text-[4rem]" />
</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 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 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"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content"
@click.stop
@click="close"
/>
</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
// where they couldn't take action because they couldn't unlock that identity.)
// check for the secret in storage
async function useSecretAndInitializeAccountsDB(
secretDB: SecretDexie,
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
export async function logConsoleAndDb(
message: string,
@@ -224,16 +239,5 @@ export async function logConsoleAndDb(
} else {
logger.log(`${new Date().toISOString()} ${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 });
await logToDb(message);
}

View File

@@ -99,7 +99,6 @@ export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}
/**
* from https://tools.ietf.org/html/rfc3986#section-3
* 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 Notifications from "notiwind";
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 { logger } from "./utils/logger";

View File

@@ -157,6 +157,11 @@ const routes: Array<RouteRecordRaw> = [
name: "InviteOneAcceptView",
component: () => import("../views/InviteOneAcceptView.vue"),
},
{
path: "/logs",
name: "logs",
component: () => import("../views/LogView.vue"),
},
{
path: "/new-activity",
name: "new-activity",
@@ -281,6 +286,15 @@ const routes: Array<RouteRecordRaw> = [
name: "user-profile",
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:";

View File

@@ -29,7 +29,12 @@
*/
import { Router } from "vue-router";
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
import {
deepLinkSchemas,
baseUrlSchema,
routeSchema,
DeepLinkRoute,
} from "../types/deepLinks";
import { logConsoleAndDb } from "../db";
import type { DeepLinkError } from "../interfaces/deepLinks";
@@ -111,7 +116,7 @@ export class DeepLinkHandler {
): Promise<void> {
const routeMap: Record<string, string> = {
"user-profile": "user-profile",
project: "project",
"project-details": "project-details",
"onboard-meeting-setup": "onboard-meeting-setup",
"invite-one-accept": "invite-one-accept",
"contact-import": "contact-import",
@@ -124,25 +129,63 @@ export class DeepLinkHandler {
did: "did",
};
const routeName = routeMap[path];
if (!routeName) {
// First try to validate the route path
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 {
code: "INVALID_ROUTE",
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 validatedParams = await schema.parseAsync({
...params,
...query,
});
await this.router.replace({
name: routeName,
params: validatedParams,
query,
});
try {
const validatedParams = await schema.parseAsync({
...params,
...query,
});
await this.router.replace({
name: routeName,
params: validatedParams,
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";
// 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({
scheme: z.literal("timesafari"),
path: z.string(),
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
export const deepLinkSchemas = {
"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 = {
log: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.log(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
}
},
warn: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
}
},
error: (message: string, ...args: unknown[]) => {
// Errors will always be logged
// 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
id="switch-identity-link"
: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
</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">
<h2 class="text-slate-500 text-sm font-bold">
Import Contacts & Settings Database
@@ -856,17 +867,6 @@
</div>
</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">
<span>
<span class="text-slate-500 text-sm font-bold mb-2">
@@ -912,6 +912,13 @@
/>
</div>
</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>
</section>
</template>
@@ -1071,7 +1078,7 @@ export default class AccountViewView extends Vue {
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
this.apiServer +
this.partnerApiServer +
"/api/partner/userProfileForIssuer/" +
this.activeDid,
{ headers },
@@ -1889,7 +1896,7 @@ export default class AccountViewView extends Vue {
);
}
const response = await this.axios.post(
this.apiServer + "/api/partner/userProfile",
this.partnerApiServer + "/api/partner/userProfile",
payload,
{ headers },
);
@@ -1977,7 +1984,7 @@ export default class AccountViewView extends Vue {
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.delete(
this.apiServer + "/api/partner/userProfile",
this.partnerApiServer + "/api/partner/userProfile",
{ headers },
);
if (response.status === 204) {

View File

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

View File

@@ -5,9 +5,9 @@
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<div class="relative px-7">
<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()"
>
<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>
<!-- 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">
<h2 class="text-xl font-bold">
<h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity
<button @click="openFeedFilters()">
<span class="text-xs text-white">
<font-awesome
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-1 py-1.5 rounded-md"
/>
<font-awesome
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-1 py-1.5 rounded-md"
/>
</span>
<button
v-if="resultsAreFiltered()"
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"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
<button
v-else
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"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
</h2>
</div>
@@ -250,84 +250,20 @@
</div>
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="border-t border-slate-300">
<li
<ul id="listLatestActivity" class="space-y-4">
<ActivityListItem
v-for="record in feedData"
:key="record.jwtId"
class="border-b border-slate-300 py-2"
>
<div
v-if="record.jwtId == feedLastViewedClaimId"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
>
You've already seen all the following
</div>
<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>
:record="record"
:last-viewed-claim-id="feedLastViewedClaimId"
:is-registered="isRegistered"
:active-did="activeDid"
:confirmer-id-list="record.confirmerIdList"
@load-claim="onClickLoadClaim"
@view-image="openImageViewer"
@cache-image="cacheImageData"
@confirm-claim="confirmClaim"
/>
</ul>
</InfiniteScroll>
<div v-if="isFeedLoading">
@@ -369,6 +305,7 @@ import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
import ImageViewer from "../components/ImageViewer.vue";
import ActivityListItem from "../components/ActivityListItem.vue";
import {
AppString,
NotificationIface,
@@ -403,23 +340,11 @@ import {
OnboardPage,
} from "../libs/util";
import { GiveSummaryRecord } from "../interfaces";
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
jwtId: string;
giver: {
displayName: string;
known: boolean;
profileImageUrl?: string;
};
image?: string;
providerPlanName?: string;
recipientProjectName?: string;
receiver: {
displayName: string;
known: boolean;
profileImageUrl?: string;
};
}
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "types";
/**
* HomeView - Main view component for the application's home page
*
@@ -442,6 +367,7 @@ import { logger } from "../utils/logger";
TopMessage,
UserNameDialog,
ImageViewer,
ActivityListItem,
},
})
export default class HomeView extends Vue {
@@ -522,10 +448,88 @@ export default class HomeView extends Vue {
this.isCreatingIdentifier = false;
this.allMyDids = [newDid];
}
} catch (error) {
logConsoleAndDb(
"Error retrieving all account DIDs on home page:" + error,
true,
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
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 +806,12 @@ export default class HomeView extends Vue {
this.allMyDids,
),
image: claim.image,
issuer: didInfoForContact(
record.issuerDid,
this.activeDid,
contactForDid(record.issuerDid, this.allContacts),
this.allMyDids,
),
providerPlanHandleId: provider?.identifier as string,
providerPlanName: providedByPlan?.name as string,
recipientProjectName: fulfillsPlan?.name as string,
@@ -1079,5 +1089,67 @@ export default class HomeView extends Vue {
this.selectedImage = imageUrl;
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>

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

View File

@@ -74,7 +74,7 @@ execute_deeplink() {
echo "---"
else
# Stop the app before executing the deep link
adb shell am force-stop app.timesafari
adb shell am force-stop app.timesafari.app
sleep 1 # Give it a moment to fully stop
if adb shell am start -W -a android.intent.action.VIEW \

6
vite.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
preview: {
host: true,
port: 4173
}
}