Compare commits
31 Commits
deep_linki
...
app_id_fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd32c37cfb | ||
| 071792b97c | |||
| bf2f23021f | |||
| 829870b16c | |||
|
|
44ffeebabe | ||
|
|
bed3bfa387 | ||
| b1056fc8dd | |||
| 189bfabcf8 | |||
|
|
aed1a9fea8 | ||
|
|
f71c76fcd3 | ||
|
|
d024db2258 | ||
|
|
c760385dcf | ||
|
|
8be8de5f1f | ||
|
|
b40604f8a6 | ||
|
|
2660b91995 | ||
|
|
474999dc9c | ||
| e825950e6e | |||
| a73d0a85e2 | |||
| fc01e81af7 | |||
|
|
436f40813c | ||
|
|
77b296b606 | ||
|
|
683e85f5be | ||
| e3ac5fe9fe | |||
|
|
8f7d794962 | ||
|
|
fa7d6317b9 | ||
|
|
4a75cdf20e | ||
|
|
79fdb9e570 | ||
|
|
aa09827317 | ||
|
|
cc1780bd01 | ||
|
|
e5d9c25ad4 | ||
|
|
ef8c2e6093 |
76
BUILDING.md
76
BUILDING.md
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -1,3 +0,0 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
1
android/app/.gitignore
vendored
1
android/app/.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
src/main/assets/public/assets/
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 "UnsanitizedFilenameFromContentProvider""
|
||||
errorLine1=" disable 'UnsanitizedFilenameFromContentProvider'"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle"
|
||||
line="26"
|
||||
column="18"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnknownIssueId"
|
||||
message="Unknown issue id "UnsanitizedFilenameFromContentProvider""
|
||||
errorLine1=" disable 'UnsanitizedFilenameFromContentProvider'"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle"
|
||||
line="26"
|
||||
column="18"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnknownIssueId"
|
||||
message="Unknown issue id "LintBaselineFixed""
|
||||
errorLine1=" ignore 'LintBaselineFixed'"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle"
|
||||
line="34"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnknownIssueId"
|
||||
message="Unknown issue id "LintBaselineFixed""
|
||||
errorLine1=" ignore 'LintBaselineFixed'"
|
||||
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("Accept") && header.getValue().toLowerCase().contains("text/html")) {"
|
||||
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("yyyyMMdd_HHmmss").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("yyyy-MM-dd'T'HH:mm'Z'");"
|
||||
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="<application android:usesCleartextTraffic="true">"
|
||||
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=" <intent-filter android:autoVerify="true">"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="25"
|
||||
column="28"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ManifestOrder"
|
||||
message="`<uses-permission>` tag appears after `<application>` tag"
|
||||
errorLine1=" <uses-permission android:name="android.permission.INTERNET" />"
|
||||
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 'com.android.tools.build:gradle:8.2.1'"
|
||||
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 'com.android.tools.build:gradle:8.2.1'"
|
||||
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 "androidx.appcompat:appcompat:$androidxAppCompatVersion""
|
||||
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 "androidx.appcompat:appcompat:$androidxAppCompatVersion""
|
||||
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 "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion""
|
||||
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 "androidx.test.ext:junit:$androidxJunitVersion""
|
||||
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 "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion""
|
||||
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 "androidx.appcompat:appcompat:$androidxAppCompatVersion""
|
||||
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 "androidx.test.ext:junit:$androidxJunitVersion""
|
||||
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 "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion""
|
||||
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="#F0FF1414""
|
||||
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="<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
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="<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">"
|
||||
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="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
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="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
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=" <string name="package_name">app.timesafari.app</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=" <string name="custom_url_scheme">app.timesafari.app</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="<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">"
|
||||
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="<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">"
|
||||
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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"appId": "app.timesafari.app",
|
||||
"appId": "app.timesafari",
|
||||
"appName": "TimeSafari",
|
||||
"webDir": "dist",
|
||||
"bundledWebRuntime": false,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.timesafari;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
// ... existing code ...
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.timesafari.app;
|
||||
package timesafari.app;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
android {
|
||||
lintOptions {
|
||||
disable 'UnsanitizedFilenameFromContentProvider'
|
||||
abortOnError false
|
||||
baseline file("lint-baseline.xml")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
assets/icon-only.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
@@ -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
3824
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
|
||||
|
||||
265
src/components/ActivityListItem.vue
Normal file
265
src/components/ActivityListItem.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
164
src/main.ts
164
src/main.ts
@@ -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";
|
||||
|
||||
|
||||
@@ -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:";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
25
src/types/index.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
232
src/views/DeepLinkErrorView.vue
Normal file
232
src/views/DeepLinkErrorView.vue
Normal 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>
|
||||
@@ -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
98
src/views/LogView.vue
Normal 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>
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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 "======================================"
|
||||
@@ -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).
|
||||
|
||||
@@ -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
6
vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
preview: {
|
||||
host: true,
|
||||
port: 4173
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user