fix: Fix onboard-meeting-members deep link with groupId. #172

Merged
trentlarson merged 14 commits from fix-deep-link into master 2 days ago
  1. 23
      .cursor/rules/development/type_safety_guide.mdc
  2. 57
      .cursor/rules/software_development.mdc
  3. 75
      BUILDING.md
  4. 8
      CHANGELOG.md
  5. 27
      README.md
  6. 4
      android/app/build.gradle
  7. 8
      ios/App/App.xcodeproj/project.pbxproj
  8. 4
      package-lock.json
  9. 10
      package.json
  10. 34
      scripts/build-android.sh
  11. 8
      scripts/build-ios.sh
  12. 110
      scripts/check-dependencies.sh
  13. 62
      scripts/clean-android.sh
  14. 28
      src/interfaces/deepLinks.ts
  15. 45
      src/services/ProfileService.ts
  16. 30
      src/services/deepLinks.ts
  17. 1
      src/views/AccountViewView.vue
  18. 4
      src/views/DeepLinkErrorView.vue
  19. 2
      src/views/OnboardMeetingMembersView.vue

23
.cursor/rules/development/type_safety_guide.mdc

@ -46,27 +46,18 @@ Practical rules to keep TypeScript strict and predictable. Minimize exceptions.
### Core Type Safety Rules ### Core Type Safety Rules
- **No `any` Types**: Use explicit types or `unknown` with proper type guards - **No `any` Types**: Use explicit types or `unknown` with proper type guards
- **Error Handling Uses Guards**: Implement and reuse type guards from - **Error Handling Uses Guards**: Implement and reuse type guards from `src/interfaces/**`
`src/interfaces/**` - **Dynamic Property Access**: Use `keyof` + `in` checks for type-safe property access
- **Dynamic Property Access**: Use `keyof` + `in` checks for type-safe
property access
### Type Guard Patterns ### Type Guard Patterns
- **API Errors**: Use `isApiError(error)` guards for API error handling - **API Errors**: Use `isApiError(error)` guards for API error handling
- **Database Errors**: Use `isDatabaseError(error)` guards for database - **Database Errors**: Use `isDatabaseError(error)` guards for database operations
operations - **Axios Errors**: Implement `isAxiosError(error)` guards for HTTP error handling
- **Axios Errors**: Implement `isAxiosError(error)` guards for HTTP error
handling
### Implementation Guidelines ### Implementation Guidelines
- **Avoid Type Assertions**: Replace `as any` with proper type guards and interfaces
- **Avoid Type Assertions**: Replace `as any` with proper type guards and - **Narrow Types Properly**: Use type guards to narrow `unknown` types safely
interfaces - **Document Type Decisions**: Explain complex type structures and their purpose
- **Narrow Types Properly**: Use type guards to narrow `unknown` types
safely
- **Document Type Decisions**: Explain complex type structures and their
purpose
## Minimal Special Cases (document in PR when used) ## Minimal Special Cases (document in PR when used)

57
.cursor/rules/software_development.mdc

@ -171,10 +171,55 @@ debugging, architecture decisions, and testing.
- [ ] Environment impact assessed for team members - [ ] Environment impact assessed for team members
- [ ] Pre-build validation implemented where appropriate - [ ] Pre-build validation implemented where appropriate
--- ## Additional Core Principles
**Status**: Active development guidelines ### 4. Dependency Management & Environment Validation
**Priority**: High - **Pre-build Validation**: Always validate critical dependencies before executing build scripts
**Estimated Effort**: Ongoing - **Environment Consistency**: Ensure team members have identical development environments
**Dependencies**: base_context.mdc, research_diagnostic.mdc - **Dependency Verification**: Check that required packages are installed and accessible
**Stakeholders**: Development team - **Path Resolution**: Use `npx` for local dependencies to avoid PATH issues
## Additional Required Workflows
### Dependency Validation (Before Proposing Changes)
- [ ] **Dependency Validation**: Verify all required dependencies are available and accessible
### Environment Impact Assessment (During Solution Design)
- [ ] **Environment Impact**: Assess how changes affect team member setups
## Additional Competence Hooks
### Dependency & Environment Management
- **"What dependencies does this feature require and are they properly declared?"**
- **"How will this change affect team member development environments?"**
- **"What validation can we add to catch dependency issues early?"**
## Dependency Management Best Practices
### Pre-build Validation
- **Check Critical Dependencies**: Validate essential tools before executing build scripts
- **Use npx for Local Dependencies**: Prefer `npx tsx` over direct `tsx` to avoid PATH issues
- **Environment Consistency**: Ensure all team members have identical dependency versions
### Common Pitfalls
- **Missing npm install**: Team members cloning without running `npm install`
- **PATH Issues**: Direct command execution vs. npm script execution differences
- **Version Mismatches**: Different Node.js/npm versions across team members
### Validation Strategies
- **Dependency Check Scripts**: Implement pre-build validation for critical dependencies
- **Environment Requirements**: Document and enforce minimum Node.js/npm versions
- **Onboarding Checklist**: Standardize team member setup procedures
### Error Messages and Guidance
- **Specific Error Context**: Provide clear guidance when dependency issues occur
- **Actionable Solutions**: Direct users to specific commands (`npm install`, `npm run check:dependencies`)
- **Environment Diagnostics**: Implement comprehensive environment validation tools
### Build Script Enhancements
- **Early Validation**: Check dependencies before starting build processes
- **Graceful Degradation**: Continue builds when possible but warn about issues
- **Helpful Tips**: Remind users about dependency management best practices
- **Narrow Types Properly**: Use type guards to narrow `unknown` types safely
- **Document Type Decisions**: Explain complex type structures and their purpose

75
BUILDING.md

@ -1017,47 +1017,27 @@ If you need to build manually or want to understand the individual steps:
export GEM_PATH=$shortened_path export GEM_PATH=$shortened_path
``` ```
1. Build the web assets & update ios: 1. Bump the version in package.json, then here.
```bash
rm -rf dist
npm run build:web
npm run build:capacitor
npx cap sync ios
```
- If that fails with "Could not find..." then look at the "gem_path" instructions above.
3. Copy the assets:
```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
npx capacitor-assets generate --ios
``` ```
cd ios/App && xcrun agvtool new-version 40 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.7;/g" App.xcodeproj/project.pbxproj && cd -
4. Bump the version to match Android & package.json:
```
cd ios/App && xcrun agvtool new-version 39 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.6;/g" App.xcodeproj/project.pbxproj && cd -
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
``` ```
5. Open the project in Xcode: 2. Build.
```bash Here's prod. Also available: test, dev
npx cap open ios
``` ```bash
npm run build:ios:prod
```
6. Use Xcode to build and run on simulator or device. 3.1. Use Xcode to build and run on simulator or device.
* Select Product -> Destination with some Simulator version. Then click the run arrow. * Select Product -> Destination with some Simulator version. Then click the run arrow.
7. Release 3.2. Use Xcode to release.
* Someday: Under "General" we want to rename a bunch of things to "Time Safari" * Someday: Under "General" we want to rename a bunch of things to "Time Safari"
* Choose Product -> Destination -> Any iOS Device * Choose Product -> Destination -> Any iOS Device
@ -1125,35 +1105,28 @@ The recommended way to build for Android is using the automated build script:
#### Manual Build Process #### Manual Build Process
1. Build the web assets: 1. Bump the version in package.json, then here: android/app/build.gradle
```bash
rm -rf dist
npm run build:web
npm run build:capacitor
```
2. Update Android project with latest build:
```bash ```bash
npx cap sync android perl -p -i -e 's/versionCode .*/versionCode 40/g' android/app/build.gradle
``` perl -p -i -e 's/versionName .*/versionName "1.0.7"/g' android/app/build.gradle
```
3. Copy the assets 2. Build.
```bash Here's prod. Also available: test, dev
npx capacitor-assets generate --android
```
4. Bump version to match iOS & package.json: android/app/build.gradle ```bash
npm run build:android:prod
```
5. Open the project in Android Studio: 3. Open the project in Android Studio:
```bash ```bash
npx cap open android npx cap open android
``` ```
6. Use Android Studio to build and run on emulator or device. 4. Use Android Studio to build and run on emulator or device.
## Android Build from the console ## Android Build from the console
@ -1186,9 +1159,9 @@ cd -
At play.google.com/console: At play.google.com/console:
- Go to the Testing Track (eg. Closed). - Go to Production or the Closed Testing and either Create Track or Manage Track.
- Click "Create new release". - Click "Create new release".
- Upload the `aab` file. - Upload the `aab` file from: app/build/outputs/bundle/release/app-release.aab
- Hit "Next". - Hit "Next".
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review". - Save, go to "Publishing Overview" as prompted, and click "Send changes for review".

8
CHANGELOG.md

@ -5,12 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.3] - 2025.07.12 ## [1.0.7] - 2025.08.18
### Changed
- Photo is pinned to profile mode
### Fixed ### Fixed
- Deep link URLs (and other prod settings) - Deep link for onboard-meeting-members
- Error in BVC begin view
## [1.0.6] - 2025.08.09 ## [1.0.6] - 2025.08.09
### Fixed ### Fixed

27
README.md

@ -170,6 +170,33 @@ npm run assets:clean
npm run build:native npm run build:native
``` ```
### Environment Setup & Dependencies
Before building the application, ensure your development environment is properly
configured:
```bash
# Install all dependencies (required first time and after updates)
npm install
# Validate your development environment
npm run check:dependencies
# Check prerequisites for testing
npm run test:prerequisites
```
**Common Issues & Solutions**:
- **"tsx: command not found"**: Run `npm install` to install devDependencies
- **"capacitor-assets: command not found"**: Ensure `@capacitor/assets` is installed
- **Build failures**: Run `npm run check:dependencies` to diagnose environment issues
**Required Versions**:
- Node.js: 18+ (LTS recommended)
- npm: 8+ (comes with Node.js)
- Platform-specific tools: Android Studio, Xcode (for mobile builds)
### Platform Support ### Platform Support
- **Android**: Adaptive icons with foreground/background, monochrome support - **Android**: Adaptive icons with foreground/background, monochrome support

4
android/app/build.gradle

@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 39 versionCode 40
versionName "1.0.6" versionName "1.0.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

8
ios/App/App.xcodeproj/project.pbxproj

@ -403,7 +403,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 39; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@ -413,7 +413,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.6; MARKETING_VERSION = 1.0.7;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -430,7 +430,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 39; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@ -440,7 +440,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.6; MARKETING_VERSION = 1.0.7;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.7-beta", "version": "1.0.8-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.0.7-beta", "version": "1.0.8-beta",
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",

10
package.json

@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.7-beta", "version": "1.0.8-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@ -12,6 +12,8 @@
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js", "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"test:prerequisites": "node scripts/check-prerequisites.js", "test:prerequisites": "node scripts/check-prerequisites.js",
"check:dependencies": "./scripts/check-dependencies.sh",
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on", "test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "./scripts/test-mobile.sh", "test:mobile": "./scripts/test-mobile.sh",
"test:android": "node scripts/test-android.js", "test:android": "node scripts/test-android.js",
@ -27,8 +29,8 @@
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts", "build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync", "build:capacitor:sync": "npm run build:capacitor && npx cap sync",
"build:native": "vite build && npx cap sync && npx capacitor-assets generate", "build:native": "vite build && npx cap sync && npx capacitor-assets generate",
"assets:config": "tsx scripts/assets-config.ts", "assets:config": "npx tsx scripts/assets-config.ts",
"assets:validate": "tsx scripts/assets-validator.ts", "assets:validate": "npx tsx scripts/assets-validator.ts",
"assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true", "assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true",
"build:ios": "./scripts/build-ios.sh", "build:ios": "./scripts/build-ios.sh",
"build:ios:dev": "./scripts/build-ios.sh --dev", "build:ios:dev": "./scripts/build-ios.sh --dev",
@ -96,7 +98,7 @@
"build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg", "build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg",
"build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg", "build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg",
"build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg", "build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg",
"clean:android": "adb uninstall app.timesafari.app || true", "clean:android": "./scripts/clean-android.sh",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true", "clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean", "clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron", "clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",

34
scripts/build-android.sh

@ -49,6 +49,31 @@ set -e
# Source common utilities # Source common utilities
source "$(dirname "$0")/common.sh" source "$(dirname "$0")/common.sh"
# Function to validate critical dependencies
validate_dependencies() {
log_info "Validating critical dependencies..."
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
log_error "node_modules directory not found. Please run 'npm install' first."
exit 1
fi
# Check if tsx is available
if [ ! -f "node_modules/.bin/tsx" ]; then
log_error "tsx dependency not found. Please run 'npm install' first."
exit 1
fi
# Check if capacitor-assets is available
if [ ! -f "node_modules/.bin/capacitor-assets" ]; then
log_error "capacitor-assets dependency not found. Please run 'npm install' first."
exit 1
fi
log_success "All critical dependencies validated successfully"
}
# Default values # Default values
BUILD_MODE="development" BUILD_MODE="development"
BUILD_TYPE="debug" BUILD_TYPE="debug"
@ -179,6 +204,11 @@ parse_android_args "$@"
# Print build header # Print build header
print_header "TimeSafari Android Build Process" print_header "TimeSafari Android Build Process"
# Validate dependencies before proceeding
validate_dependencies
# Log build start
log_info "Starting Android build process at $(date)" log_info "Starting Android build process at $(date)"
log_info "Build mode: $BUILD_MODE" log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE" log_info "Build type: $BUILD_TYPE"
@ -257,6 +287,7 @@ fi
# Step 1: Validate asset configuration # Step 1: Validate asset configuration
safe_execute "Validating asset configuration" "npm run assets:validate" || { safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_warn "Asset validation found issues, but continuing with build..." log_warn "Asset validation found issues, but continuing with build..."
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
} }
# Step 2: Clean Android app # Step 2: Clean Android app
@ -337,6 +368,9 @@ if [ "$OPEN_STUDIO" = true ]; then
log_info "Android Studio: opened" log_info "Android Studio: opened"
fi fi
# Reminder about dependency management
log_info "💡 Tip: If you encounter dependency issues, run 'npm install' to ensure all packages are up to date."
print_footer "Android Build" print_footer "Android Build"
# Exit with success # Exit with success

8
scripts/build-ios.sh

@ -173,20 +173,20 @@ check_ios_resources() {
# Check for required assets # Check for required assets
if [ ! -f "assets/icon.png" ]; then if [ ! -f "assets/icon.png" ]; then
log_warning "App icon not found at assets/icon.png" log_warn "App icon not found at assets/icon.png"
fi fi
if [ ! -f "assets/splash.png" ]; then if [ ! -f "assets/splash.png" ]; then
log_warning "Splash screen not found at assets/splash.png" log_warn "Splash screen not found at assets/splash.png"
fi fi
# Check for iOS-specific files # Check for iOS-specific files
if [ ! -f "ios/App/App/Info.plist" ]; then if [ ! -f "ios/App/App/Info.plist" ]; then
log_warning "Info.plist not found" log_warn "Info.plist not found"
fi fi
if [ ! -f "ios/App/App/AppDelegate.swift" ]; then if [ ! -f "ios/App/App/AppDelegate.swift" ]; then
log_warning "AppDelegate.swift not found" log_warn "AppDelegate.swift not found"
fi fi
log_success "iOS resource check completed" log_success "iOS resource check completed"

110
scripts/check-dependencies.sh

@ -0,0 +1,110 @@
#!/bin/bash
# check-dependencies.sh
# Author: Matthew Raymer
# Date: 2025-08-19
# Description: Dependency validation script for TimeSafari development environment
# This script checks for critical dependencies required for building the application.
# Exit on any error
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
print_header "TimeSafari Dependency Validation"
log_info "Checking development environment dependencies..."
# Check Node.js version
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version)
log_info "Node.js version: $NODE_VERSION"
# Extract major version number
MAJOR_VERSION=$(echo $NODE_VERSION | sed 's/v\([0-9]*\)\..*/\1/')
if [ "$MAJOR_VERSION" -lt 18 ]; then
log_error "Node.js version $NODE_VERSION is too old. Please upgrade to Node.js 18 or later."
exit 1
fi
else
log_error "Node.js is not installed. Please install Node.js 18 or later."
exit 1
fi
# Check npm version
if command -v npm &> /dev/null; then
NPM_VERSION=$(npm --version)
log_info "npm version: $NPM_VERSION"
else
log_error "npm is not installed. Please install npm."
exit 1
fi
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
log_error "node_modules directory not found."
log_info "Please run: npm install"
exit 1
fi
# Check critical dependencies
log_info "Validating critical packages..."
CRITICAL_DEPS=("tsx" "capacitor-assets" "vite")
for dep in "${CRITICAL_DEPS[@]}"; do
if [ -f "node_modules/.bin/$dep" ]; then
log_success "$dep found"
else
log_error "$dep not found in node_modules/.bin"
log_info "This usually means the package wasn't installed properly."
log_info "Try running: npm install"
exit 1
fi
done
# Check TypeScript via npx
if npx tsc --version &> /dev/null; then
TSC_VERSION=$(npx tsc --version)
log_success "✓ TypeScript found: $TSC_VERSION"
else
log_error "✗ TypeScript not accessible via npx"
log_info "Try running: npm install"
exit 1
fi
# Check Capacitor CLI
if command -v npx &> /dev/null; then
if npx cap --version &> /dev/null; then
CAP_VERSION=$(npx cap --version)
log_success "✓ Capacitor CLI version: $CAP_VERSION"
else
log_error "✗ Capacitor CLI not accessible via npx"
log_info "Try running: npm install @capacitor/cli"
exit 1
fi
else
log_error "npx is not available. Please ensure npm is properly installed."
exit 1
fi
# Check Android development tools
if command -v adb &> /dev/null; then
log_success "✓ Android Debug Bridge (adb) found"
else
log_warn "⚠ Android Debug Bridge (adb) not found"
log_info "This is only needed for Android development and testing."
fi
if command -v gradle &> /dev/null; then
GRADLE_VERSION=$(gradle --version | head -n 1)
log_success "✓ Gradle found: $GRADLE_VERSION"
else
log_warn "⚠ Gradle not found in PATH"
log_info "This is only needed if building outside of Android Studio."
fi
log_success "Dependency validation completed successfully!"
log_info "Your development environment is ready for TimeSafari development."
print_footer "Dependency Validation"

62
scripts/clean-android.sh

@ -0,0 +1,62 @@
#!/bin/bash
# clean-android.sh
# Author: Matthew Raymer
# Date: 2025-08-19
# Description: Clean Android app with timeout protection to prevent hanging
# This script safely uninstalls the TimeSafari app from connected Android devices
# with a 30-second timeout to prevent indefinite hanging.
# Exit on any error
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Function to implement timeout for systems without timeout command
timeout_command() {
local timeout_seconds="$1"
shift
# Check if timeout command exists
if command -v timeout &> /dev/null; then
timeout "$timeout_seconds" "$@"
else
# Fallback for systems without timeout (like macOS)
# Use perl to implement timeout
perl -e '
eval {
local $SIG{ALRM} = sub { die "timeout" };
alarm shift;
system @ARGV;
alarm 0;
};
if ($@) { exit 1; }
' "$timeout_seconds" "$@"
fi
}
log_info "Starting Android cleanup process..."
# Check if adb is available
if ! command -v adb &> /dev/null; then
log_error "adb command not found. Please install Android SDK Platform Tools."
exit 1
fi
# Check for connected devices
log_info "Checking for connected Android devices..."
if adb devices | grep -q 'device$'; then
log_info "Android device(s) found. Attempting to uninstall app..."
# Try to uninstall with timeout
if timeout_command 30 adb uninstall app.timesafari.app; then
log_success "Successfully uninstalled TimeSafari app"
else
log_warn "Uninstall failed or timed out after 30 seconds"
log_info "This is normal if the app wasn't installed or device is unresponsive"
fi
else
log_info "No Android devices connected. Skipping uninstall."
fi
log_success "Android cleanup process completed"

28
src/interfaces/deepLinks.ts

@ -28,7 +28,7 @@
import { z } from "zod"; import { z } from "zod";
// Parameter validation schemas for each route type // Parameter validation schemas for each route type
export const deepLinkSchemas = { export const deepLinkPathSchemas = {
claim: z.object({ claim: z.object({
id: z.string(), id: z.string(),
}), }),
@ -60,7 +60,7 @@ export const deepLinkSchemas = {
jwt: z.string().optional(), jwt: z.string().optional(),
}), }),
"onboard-meeting-members": z.object({ "onboard-meeting-members": z.object({
id: z.string(), groupId: z.string(),
}), }),
project: z.object({ project: z.object({
id: z.string(), id: z.string(),
@ -70,6 +70,17 @@ export const deepLinkSchemas = {
}), }),
}; };
export const deepLinkQuerySchemas = {
"onboard-meeting-members": z.object({
password: z.string(),
}),
};
// Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = Object.keys(
deepLinkPathSchemas,
) as readonly (keyof typeof deepLinkPathSchemas)[];
// Create a type from the array // Create a type from the array
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
@ -80,14 +91,13 @@ export const baseUrlSchema = z.object({
queryParams: z.record(z.string()).optional(), queryParams: z.record(z.string()).optional(),
}); });
// Add a union type of all valid route paths // export type DeepLinkPathParams = {
export const VALID_DEEP_LINK_ROUTES = Object.keys( // [K in keyof typeof deepLinkPathSchemas]: z.infer<(typeof deepLinkPathSchemas)[K]>;
deepLinkSchemas, // };
) as readonly (keyof typeof deepLinkSchemas)[];
export type DeepLinkParams = { // export type DeepLinkQueryParams = {
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; // [K in keyof typeof deepLinkQuerySchemas]: z.infer<(typeof deepLinkQuerySchemas)[K]>;
}; // };
export interface DeepLinkError extends Error { export interface DeepLinkError extends Error {
code: string; code: string;

45
src/services/ProfileService.ts

@ -10,7 +10,6 @@ import { getHeaders, errorStringForLog } from "@/libs/endorserServer";
import { handleApiError } from "./api"; import { handleApiError } from "./api";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView"; import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { AxiosErrorResponse } from "@/interfaces/common";
/** /**
* Profile data interface * Profile data interface
@ -125,15 +124,18 @@ export class ProfileService {
async deleteProfile(activeDid: string): Promise<boolean> { async deleteProfile(activeDid: string): Promise<boolean> {
try { try {
const headers = await getHeaders(activeDid); const headers = await getHeaders(activeDid);
const response = await this.axios.delete( const url = `${this.partnerApiServer}/api/partner/userProfile`;
`${this.partnerApiServer}/api/partner/userProfile`, const response = await this.axios.delete(url, { headers });
{ headers },
);
if (response.status === 204 || response.status === 200) { if (response.status === 204 || response.status === 200) {
logger.info("Profile deleted successfully"); logger.info("Profile deleted successfully");
return true; return true;
} else { } else {
logger.error("Unexpected response status when deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
});
throw new Error( throw new Error(
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`, `Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
); );
@ -157,9 +159,11 @@ export class ProfileService {
return true; // Consider this a success if profile doesn't exist return true; // Consider this a success if profile doesn't exist
} else if (response.status === 400) { } else if (response.status === 400) {
logger.error("Bad request when deleting profile:", response.data); logger.error("Bad request when deleting profile:", response.data);
throw new Error( const errorMessage =
`Profile deletion failed: ${response.data?.error?.message || "Bad request"}`, typeof response.data === "string"
); ? response.data
: response.data?.message || "Bad request";
throw new Error(`Profile deletion failed: ${errorMessage}`);
} else if (response.status === 401) { } else if (response.status === 401) {
logger.error("Unauthorized to delete profile"); logger.error("Unauthorized to delete profile");
throw new Error("You are not authorized to delete this profile"); throw new Error("You are not authorized to delete this profile");
@ -240,10 +244,33 @@ export class ProfileService {
/** /**
* Type guard for API errors with proper typing * Type guard for API errors with proper typing
*/ */
private isApiError(error: unknown): error is AxiosErrorResponse { private isApiError(error: unknown): error is {
response?: {
status?: number;
statusText?: string;
data?: { message?: string } | string;
};
} {
return typeof error === "object" && error !== null && "response" in error; return typeof error === "object" && error !== null && "response" in error;
} }
/**
* Extract URL from AxiosError without type casting
*/
private getErrorUrl(error: unknown): string | undefined {
if (this.isAxiosError(error)) {
return error.config?.url;
}
return undefined;
}
/**
* Type guard for AxiosError
*/
private isAxiosError(error: unknown): error is AxiosError {
return error instanceof AxiosError;
}
/** /**
* Extract error URL safely from error object * Extract error URL safely from error object
*/ */

30
src/services/deepLinks.ts

@ -47,10 +47,11 @@ import { Router } from "vue-router";
import { z } from "zod"; import { z } from "zod";
import { import {
deepLinkSchemas, deepLinkPathSchemas,
baseUrlSchema, baseUrlSchema,
routeSchema, routeSchema,
DeepLinkRoute, DeepLinkRoute,
deepLinkQuerySchemas,
} from "../interfaces/deepLinks"; } from "../interfaces/deepLinks";
import type { DeepLinkError } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@ -74,7 +75,7 @@ function getFirstKeyFromZodObject(
* because "router.replace" expects the right parameter name for the route. * because "router.replace" expects the right parameter name for the route.
*/ */
export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> = export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> =
Object.entries(deepLinkSchemas).reduce( Object.entries(deepLinkPathSchemas).reduce(
(acc, [routeName, schema]) => { (acc, [routeName, schema]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>); const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
@ -198,15 +199,24 @@ export class DeepLinkHandler {
} }
// Continue with parameter validation as before... // Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; const pathSchema =
deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
const querySchema =
deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
let validatedParams; let validatedPathParams: Record<string, string> = {};
let validatedQueryParams: Record<string, string> = {};
try { try {
validatedParams = await schema.parseAsync(params); if (pathSchema) {
validatedPathParams = await pathSchema.parseAsync(params);
}
if (querySchema) {
validatedQueryParams = await querySchema.parseAsync(query);
}
} catch (error) { } catch (error) {
// For parameter validation errors, provide specific error feedback // For parameter validation errors, provide specific error feedback
logger.error( logger.error(
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, `[DeepLink] Invalid parameters for route name ${routeName} for path: ${path} ... with error: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
); );
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
@ -226,20 +236,22 @@ export class DeepLinkHandler {
try { try {
await this.router.replace({ await this.router.replace({
name: routeName, name: routeName,
params: validatedParams, params: validatedPathParams,
query: validatedQueryParams,
}); });
} catch (error) { } catch (error) {
logger.error( logger.error(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`, `[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedPathParams)} ... and query: ${JSON.stringify(validatedQueryParams)}`,
); );
// For parameter validation errors, provide specific error feedback // For parameter validation errors, provide specific error feedback
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params: validatedParams, params: validatedPathParams,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "ROUTING_ERROR", errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`, errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
...validatedQueryParams,
}, },
}); });
} }

1
src/views/AccountViewView.vue

@ -182,7 +182,6 @@
@change="onLocationCheckboxChange" @change="onLocationCheckboxChange"
/> />
<label for="includeUserProfileLocation">Include Location</label> <label for="includeUserProfileLocation">Include Location</label>
</div> </div>
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video"> <div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500"> <p class="text-sm mb-2 text-slate-500">

4
src/views/DeepLinkErrorView.vue

@ -47,7 +47,7 @@ import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { import {
VALID_DEEP_LINK_ROUTES, VALID_DEEP_LINK_ROUTES,
deepLinkSchemas, deepLinkPathSchemas,
} from "../interfaces/deepLinks"; } from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil"; import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@ -56,7 +56,7 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
// an object with the route as the key and the first param name as the value // an object with the route as the key and the first param name as the value
const deepLinkSchemaKeys = Object.fromEntries( const deepLinkSchemaKeys = Object.fromEntries(
Object.entries(deepLinkSchemas).map(([route, schema]) => { Object.entries(deepLinkPathSchemas).map(([route, schema]) => {
const param = Object.keys(schema.shape)[0]; const param = Object.keys(schema.shape)[0];
return [route, param]; return [route, param];
}), }),

2
src/views/OnboardMeetingMembersView.vue

@ -113,7 +113,7 @@ export default class OnboardMeetingMembersView extends Vue {
try { try {
// Identity creation should be handled by router guard, but keep as fallback for meeting setup // Identity creation should be handled by router guard, but keep as fallback for meeting setup
if (!this.activeDid) { if (!this.activeDid) {
logger.info( this.$logAndConsole(
"[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup", "[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup",
); );
this.activeDid = await generateSaveAndActivateIdentity(); this.activeDid = await generateSaveAndActivateIdentity();

Loading…
Cancel
Save