Browse Source

chore: initial commit

master
Matthew Raymer 6 days ago
parent
commit
1e6c4bf7fc
  1. 127
      test-apps/.gitignore
  2. 280
      test-apps/EMULATOR_TROUBLESHOOTING.md
  3. 326
      test-apps/README.md
  4. 384
      test-apps/SETUP_GUIDE.md
  5. 20
      test-apps/android-test/.eslintrc.cjs
  6. 105
      test-apps/android-test/.gitignore
  7. 160
      test-apps/android-test/GRADLE_TROUBLESHOOTING.md
  8. 329
      test-apps/android-test/README.md
  9. 25
      test-apps/android-test/capacitor.config.ts
  10. 90
      test-apps/android-test/env.d.ts
  11. 73
      test-apps/android-test/index.html
  12. 42
      test-apps/android-test/package.json
  13. 147
      test-apps/android-test/src/App.vue
  14. 163
      test-apps/android-test/src/components/cards/ActionCard.vue
  15. 95
      test-apps/android-test/src/components/cards/InfoCard.vue
  16. 181
      test-apps/android-test/src/components/cards/NotificationCard.vue
  17. 264
      test-apps/android-test/src/components/cards/StatusCard.vue
  18. 141
      test-apps/android-test/src/components/items/ActivityItem.vue
  19. 170
      test-apps/android-test/src/components/items/HistoryItem.vue
  20. 99
      test-apps/android-test/src/components/items/StatusItem.vue
  21. 75
      test-apps/android-test/src/components/layout/AppFooter.vue
  22. 239
      test-apps/android-test/src/components/layout/AppHeader.vue
  23. 42
      test-apps/android-test/src/main.ts
  24. 109
      test-apps/android-test/src/router/index.ts
  25. 299
      test-apps/android-test/src/stores/notifications.ts
  26. 144
      test-apps/android-test/src/views/HistoryView.vue
  27. 260
      test-apps/android-test/src/views/HomeView.vue
  28. 629
      test-apps/android-test/src/views/LogsView.vue
  29. 117
      test-apps/android-test/src/views/NotFoundView.vue
  30. 179
      test-apps/android-test/src/views/NotificationsView.vue
  31. 518
      test-apps/android-test/src/views/ScheduleView.vue
  32. 310
      test-apps/android-test/src/views/StatusView.vue
  33. 45
      test-apps/android-test/tsconfig.json
  34. 70
      test-apps/android-test/vite.config.ts
  35. 33
      test-apps/android-test/webpack.config.js
  36. 112
      test-apps/check-environment.sh
  37. 152
      test-apps/config/timesafari-config.json
  38. 8
      test-apps/daily-notification-test/.editorconfig
  39. 1
      test-apps/daily-notification-test/.gitattributes
  40. 30
      test-apps/daily-notification-test/.gitignore
  41. 48
      test-apps/daily-notification-test/README.md
  42. 101
      test-apps/daily-notification-test/android/.gitignore
  43. 2
      test-apps/daily-notification-test/android/app/.gitignore
  44. 54
      test-apps/daily-notification-test/android/app/build.gradle
  45. 19
      test-apps/daily-notification-test/android/app/capacitor.build.gradle
  46. 21
      test-apps/daily-notification-test/android/app/proguard-rules.pro
  47. 26
      test-apps/daily-notification-test/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  48. 73
      test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml
  49. 5
      test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/MainActivity.java
  50. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-land-hdpi/splash.png
  51. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-land-mdpi/splash.png
  52. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-land-xhdpi/splash.png
  53. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-land-xxhdpi/splash.png
  54. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  55. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-port-hdpi/splash.png
  56. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-port-mdpi/splash.png
  57. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-port-xhdpi/splash.png
  58. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-port-xxhdpi/splash.png
  59. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
  60. 34
      test-apps/daily-notification-test/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  61. 170
      test-apps/daily-notification-test/android/app/src/main/res/drawable/ic_launcher_background.xml
  62. BIN
      test-apps/daily-notification-test/android/app/src/main/res/drawable/splash.png
  63. 12
      test-apps/daily-notification-test/android/app/src/main/res/layout/activity_main.xml
  64. 5
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  65. 5
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  66. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  67. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  68. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  69. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  70. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  71. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  72. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  73. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  74. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  75. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  76. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  77. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  78. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  79. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  80. BIN
      test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  81. 4
      test-apps/daily-notification-test/android/app/src/main/res/values/ic_launcher_background.xml
  82. 7
      test-apps/daily-notification-test/android/app/src/main/res/values/strings.xml
  83. 22
      test-apps/daily-notification-test/android/app/src/main/res/values/styles.xml
  84. 5
      test-apps/daily-notification-test/android/app/src/main/res/xml/file_paths.xml
  85. 18
      test-apps/daily-notification-test/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  86. 29
      test-apps/daily-notification-test/android/build.gradle
  87. 3
      test-apps/daily-notification-test/android/capacitor.settings.gradle
  88. 22
      test-apps/daily-notification-test/android/gradle.properties
  89. BIN
      test-apps/daily-notification-test/android/gradle/wrapper/gradle-wrapper.jar
  90. 7
      test-apps/daily-notification-test/android/gradle/wrapper/gradle-wrapper.properties
  91. 252
      test-apps/daily-notification-test/android/gradlew
  92. 94
      test-apps/daily-notification-test/android/gradlew.bat
  93. 5
      test-apps/daily-notification-test/android/settings.gradle
  94. 16
      test-apps/daily-notification-test/android/variables.gradle
  95. 14
      test-apps/daily-notification-test/capacitor.config.ts
  96. 1
      test-apps/daily-notification-test/env.d.ts
  97. 20
      test-apps/daily-notification-test/eslint.config.ts
  98. 13
      test-apps/daily-notification-test/index.html
  99. 3997
      test-apps/daily-notification-test/package-lock.json
  100. 42
      test-apps/daily-notification-test/package.json

127
test-apps/.gitignore

@ -0,0 +1,127 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Build outputs
dist/
build/
*.tsbuildinfo
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs/
*.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output/
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Capacitor specific
android/app/build/
android/build/
android/.gradle/
android/gradle/
android/gradlew
android/gradlew.bat
android/local.properties
android/capacitor.settings.gradle
android/variables.gradle
# iOS specific (for future use)
ios/App/build/
ios/App/Pods/
ios/App/public/
ios/App/capacitor.config.json
ios/App/capacitor.plugins.json
# Electron specific (for future use)
electron/dist/
electron/build/
# Test coverage
coverage/
# Misc
*.tgz
*.tar.gz

280
test-apps/EMULATOR_TROUBLESHOOTING.md

@ -1,280 +0,0 @@
# Android Emulator GPU Troubleshooting Guide
## Overview
This guide helps resolve GPU binding issues when running Android emulators on Linux systems with NVIDIA graphics cards. The most common issue is when the emulator detects the NVIDIA GPU for Vulkan but still binds OpenGL rendering to the Intel iGPU.
## Problem Diagnosis
### GPU Binding Issues
#### Symptoms
- Emulator runs but feels sluggish
- NVIDIA GPU shows low utilization in `nvidia-smi`
- Emulator banner shows Intel graphics in OpenGL renderer:
```
OpenGL Renderer=[Android Emulator OpenGL ES Translator (Mesa Intel(R) Graphics (RPL-S))]
```
- Vulkan detection works but OpenGL compositing uses Intel
#### Root Cause
The emulator detects your NVIDIA GPU for Vulkan (`Selecting Vulkan device: NVIDIA GeForce RTX 4060...`) but the window/compositor path still binds to your Intel iGPU. Frames get decoded via gfxstream/Vulkan but final GL presentation rides the Intel driver.
### Network Connectivity Issues
#### Symptoms
- Play Services ANRs and timeouts
- "API failed to connect while resuming" errors
- `GmsClientSupervisor ... Timeout...` messages
- `Phenotype registration failed` errors
- NetworkMonitor shows hard failures:
- `ECONNREFUSED` on DNS lookups
- `UnknownHostException` for `www.google.com` / `connectivitycheck.gstatic.com`
- `[100 WIFI] validation failed`
#### Root Cause
The emulator has no working internet/DNS connectivity, causing Google apps to stall and ANR. This is often caused by:
- DNS resolution failures
- VPN/killswitch blocking emulator traffic
- Firewall rules blocking outbound connections
- Corrupted network state from previous sessions
## Solution Strategies
### 1. Network Connectivity Fixes (Priority)
If experiencing ANRs and Play Services failures, address network issues first:
#### Quick Network Fix
```bash
cd test-apps
./launch-emulator-network-fix.sh
```
#### Verify Network Status
```bash
cd test-apps
./verify-emulator-network.sh
```
#### Manual Network Verification
```bash
# Check airplane mode
adb -e shell settings get global airplane_mode_on
# Check network interfaces
adb -e shell ip addr; adb -e shell ip route
# Test DNS resolution
adb -e shell ping -c1 8.8.8.8
adb -e shell ping -c1 connectivitycheck.gstatic.com
```
#### Clear Play Services Cache
```bash
adb -e shell pm clear com.google.android.gms
adb -e shell pm clear com.android.vending
```
### 2. GPU Binding Solutions
Use the enhanced launch script with all NVIDIA offloading variables:
```bash
cd test-apps
./launch-emulator-gpu.sh
```
**What each variable does:**
- `__NV_PRIME_RENDER_OFFLOAD=1` + `__GLX_VENDOR_LIBRARY_NAME=nvidia`: Offload GL to NVIDIA
- `__VK_LAYER_NV_optimus=NVIDIA_only` + `VK_ICD_FILENAMES=...nvidia_icd.json`: Make Vulkan loader pick NVIDIA
- `DRI_PRIME=1`: Belt-and-suspenders in mixed Mesa/NVIDIA setups
- `QT_QPA_PLATFORM=xcb`: Avoids Wayland/Qt oddities
- `-no-snapshot-load`: Prevents flaky snapshots from different configs
### 2. Alternative GPU Modes
If the primary solution still binds to Intel, try these alternatives:
#### Pure OpenGL Mode
```bash
cd test-apps
./launch-emulator-opengl.sh
```
- Removes `-feature Vulkan` to avoid mixed VK+GL path
- Forces pure OpenGL rendering on NVIDIA
- Good when Vulkan+OpenGL mixed mode causes issues
#### ANGLE Mode
```bash
cd test-apps
./launch-emulator-angle.sh
```
- Uses ANGLE (Almost Native Graphics Layer Engine)
- Better NVIDIA compatibility on Linux
- More stable rendering pipeline
#### Mesa Fallback
```bash
cd test-apps
./launch-emulator-mesa.sh
```
- Software rendering as last resort
- Use only for stability testing
- More CPU intensive but very stable
### 3. Manual Launch Commands
If scripts don't work, use these manual commands:
#### Enhanced GPU Launch
```bash
QT_QPA_PLATFORM=xcb \
__NV_PRIME_RENDER_OFFLOAD=1 \
__GLX_VENDOR_LIBRARY_NAME=nvidia \
__VK_LAYER_NV_optimus=NVIDIA_only \
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json \
DRI_PRIME=1 \
emulator -avd TimeSafari_Emulator \
-gpu host \
-feature Vulkan \
-accel on \
-no-boot-anim \
-no-snapshot-load
```
#### Pure OpenGL
```bash
QT_QPA_PLATFORM=xcb \
__NV_PRIME_RENDER_OFFLOAD=1 \
__GLX_VENDOR_LIBRARY_NAME=nvidia \
DRI_PRIME=1 \
emulator -avd TimeSafari_Emulator \
-gpu host \
-accel on \
-no-boot-anim
```
#### ANGLE Mode
```bash
QT_QPA_PLATFORM=xcb \
__NV_PRIME_RENDER_OFFLOAD=1 \
__GLX_VENDOR_LIBRARY_NAME=nvidia \
DRI_PRIME=1 \
emulator -avd TimeSafari_Emulator \
-gpu angle_indirect \
-accel on \
-no-boot-anim
```
## Verification
### Check GPU Binding
After launching, check the emulator banner for:
- ✅ **Good**: `OpenGL Vendor=Google (NVIDIA)` or GL translator running atop NVIDIA
- ❌ **Bad**: `(Mesa Intel)` or Intel graphics in renderer
### Monitor GPU Usage
```bash
# Real-time GPU utilization
nvidia-smi dmon -s u
# Check emulator process specifically
watch -n1 "nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader | grep qemu"
```
You should see non-zero GPU utilization/memory for the emulator process.
## Additional Optimizations
### Clean Snapshots
Snapshots made with different configs can cause issues:
```bash
# Delete snapshots for clean state
rm -rf ~/.android/avd/TimeSafari_Emulator.avd/snapshots/
```
### Fix ADB Issues
"Device offline" after boot is common with snapshots:
```bash
adb kill-server && adb start-server
adb -e wait-for-device
```
### Disable Unnecessary Features
Save CPU, reduce log spam, and prevent hangs:
```bash
emulator -avd TimeSafari_Emulator \
-gpu host -accel on -no-boot-anim \
-feature -Bluetooth -camera-back none -camera-front none -no-audio
```
**Key Benefits:**
- `-feature -Bluetooth`: Prevents Bluetooth-related hangs and ANRs
- `-camera-back none -camera-front none`: Disables camera hardware
- `-no-audio`: Disables audio system (saves resources)
### CPU/RAM Configuration
Keep your existing settings:
```bash
emulator -avd TimeSafari_Emulator \
-cores 6 -memory 4096 \
-gpu host -accel on -no-boot-anim
```
## Troubleshooting Checklist
### Before Launch
- [ ] NVIDIA drivers installed and working
- [ ] Vulkan ICD file exists: `/usr/share/vulkan/icd.d/nvidia_icd.json`
- [ ] AVD exists and is properly configured
- [ ] Android SDK and emulator in PATH
### After Launch
- [ ] Check emulator banner for correct GPU binding
- [ ] Monitor `nvidia-smi` for GPU utilization
- [ ] Verify ADB connection with `adb devices`
- [ ] Test app installation and functionality
### If Still Having Issues
- [ ] Try different GPU modes (OpenGL, ANGLE, Mesa)
- [ ] Clean snapshots and restart
- [ ] Check system logs for GPU errors
- [ ] Verify NVIDIA driver compatibility
- [ ] Consider software rendering for stability testing
## Performance Expectations
### Hardware Acceleration (NVIDIA)
- **GPU Utilization**: 20-60% during normal use
- **Memory Usage**: 500MB-2GB GPU memory
- **UI Responsiveness**: Smooth, 60fps target
- **Startup Time**: 30-60 seconds
### Software Rendering (Mesa)
- **CPU Usage**: 50-80% on all cores
- **Memory Usage**: 1-4GB system RAM
- **UI Responsiveness**: Slower, 30fps typical
- **Startup Time**: 60-120 seconds
## System Requirements
### Minimum
- **CPU**: 4 cores, 2.5GHz+
- **RAM**: 8GB system, 4GB for emulator
- **GPU**: Any with OpenGL 3.0+ support
- **Storage**: 10GB free space
### Recommended
- **CPU**: 6+ cores, 3.0GHz+
- **RAM**: 16GB system, 6GB for emulator
- **GPU**: NVIDIA GTX 1060+ or equivalent
- **Storage**: 20GB free space, SSD preferred
## Support
If you continue experiencing issues:
1. Check the [Android Emulator documentation](https://developer.android.com/studio/run/emulator)
2. Review [NVIDIA Linux driver documentation](https://docs.nvidia.com/driver/)
3. Test with different AVD configurations
4. Consider using physical Android devices for testing

326
test-apps/README.md

@ -1,326 +0,0 @@
# TimeSafari Test Apps Setup Guide
## Overview
This guide creates minimal Capacitor test apps for validating the TimeSafari Daily Notification Plugin integration across all target platforms. The test apps demonstrate TimeSafari's community-building features, Endorser.ch API integration, and notification patterns.
## Directory Structure
```
test-apps/
├── android-test/ # Android test app
├── ios-test/ # iOS test app
├── electron-test/ # Electron test app
├── test-api/ # TimeSafari Test API server
├── shared/ # Shared configuration and utilities
│ └── config-loader.ts # Configuration loader and mock services
├── config/ # Configuration files
│ └── timesafari-config.json
├── setup-android.sh # Android setup script
├── setup-ios.sh # iOS setup script
├── setup-electron.sh # Electron setup script
├── check-environment.sh # Environment verification
├── SETUP_GUIDE.md # Enhanced setup guide
└── README.md # This guide
```
## Prerequisites
- Node.js 18+
- Capacitor CLI: `npm install -g @capacitor/cli`
- Android Studio (for Android)
- Xcode (for iOS)
- Platform-specific SDKs
- Understanding of TimeSafari's community-building purpose
- Familiarity with Endorser.ch API patterns
## Quick Start
### Option 1: Automated Setup (Recommended)
```bash
# Navigate to test-apps directory first
cd test-apps
# Setup all platforms (run from test-apps directory)
./setup-android.sh
./setup-ios.sh
./setup-electron.sh
```
**⚠️ Important**: Run setup scripts from the `test-apps` directory, not from individual platform directories.
### Option 2: Manual Setup
See [Enhanced Setup Guide](SETUP_GUIDE.md) for detailed manual setup instructions and troubleshooting.
### Prerequisites Check
```bash
# Check your environment before setup
./check-environment.sh
```
**Required Software**:
- **Node.js 18+**: Required for all platforms
- **Android Studio**: Required for Android testing
- **Xcode**: Required for iOS testing (macOS only)
- **No additional requirements**: For Electron testing
## Test App Features
Each test app includes comprehensive UI patterns and testing capabilities:
### **Core Testing Features**
- **TimeSafari Configuration**: Test community-focused notification settings
- **Generic Polling Interface**: Test new structured request/response polling system
- **Endorser.ch API Integration**: Test real API patterns with pagination
- **Community Notification Scheduling**: Test offers, projects, people, and items notifications
- **Static Daily Reminders**: Test simple daily notifications without network content
- **Performance Monitoring**: Metrics collection and display
- **Error Handling**: Comprehensive error testing
- **Debug Information**: Platform-specific debug data
### **Enhanced UI Components**
- **Permission Management**: Request dialogs, status displays, settings integration
- **Configuration Panels**: Settings toggles, time pickers, content type selection
- **Status Dashboards**: Real-time monitoring with performance metrics
- **Platform-Specific Features**:
- **Android**: Battery optimization, exact alarm permissions, reboot recovery
- **iOS**: Background app refresh, rolling window management, BGTaskScheduler
- **Electron**: Service worker status, push notifications, IPC communication
- **Error Handling UI**: User-friendly error displays with retry mechanisms
- **Testing Tools**: Test notification panels, debug info, log export
### **UI Design Features**
- **Responsive Design**: Mobile-first approach with touch-friendly interfaces
- **Accessibility**: WCAG 2.1 AA compliance with keyboard navigation
- **Platform Native**: Material Design (Android), Human Interface Guidelines (iOS)
- **Progressive Disclosure**: Essential features first, advanced options on demand
- **Real-time Updates**: Live status monitoring and performance metrics
## TimeSafari Test API Server
A comprehensive REST API server (`test-api/`) simulates Endorser.ch API endpoints for testing the plugin's TimeSafari-specific functionality:
### Quick Start
```bash
# Start the TimeSafari Test API server
cd test-apps/test-api
npm install
npm start
# Test the API
npm run demo
```
### Key Features
- **Endorser.ch API Simulation**: Mock endpoints for offers, projects, and pagination
- **TimeSafari Notification Bundle**: Single route for bundled notifications
- **Community Analytics**: Analytics endpoint for community events
- **Pagination Support**: Full afterId/beforeId pagination testing
- **ETag Support**: HTTP caching with conditional requests
- **Error Simulation**: Test various error scenarios
- **Metrics**: Monitor API usage and performance
- **CORS Enabled**: Cross-origin requests supported
### API Endpoints
#### Endorser.ch API Endpoints
- `GET /api/v2/report/offers` - Get offers to person
- `GET /api/v2/report/offersToPlansOwnedByMe` - Get offers to user's projects
- `POST /api/v2/report/plansLastUpdatedBetween` - Get changes to starred projects
#### TimeSafari API Endpoints
- `GET /api/v2/report/notifications/bundle` - Get bundled notifications
- `POST /api/analytics/community-events` - Send community analytics
#### Legacy Endpoints
- `GET /health` - Health check
- `GET /api/content/:slotId` - Get notification content
- `GET /api/metrics` - API metrics
### Platform-Specific URLs
- **Web/Electron**: `http://localhost:3001`
- **Android Emulator**: `http://10.0.2.2:3001`
- **iOS Simulator**: `http://localhost:3001`
- **Physical Devices**: `http://[YOUR_IP]:3001`
## Platform-Specific Testing
### Android Test App
- **TimeSafari Configuration**: Test community notification settings
- **Generic Polling Interface**: Test structured request/response polling with Android WorkManager
- **Endorser.ch API Integration**: Test parallel API requests
- **Exact Alarm Status**: Check permission and capability
- **Permission Requests**: Test exact alarm permission flow
- **Performance Metrics**: Monitor Android-specific optimizations
- **Reboot Recovery**: Validate system restart handling
- **Enhanced UI**: Permission dialogs, battery optimization, exact alarm management
- **Status Dashboard**: Real-time monitoring with Android-specific metrics
- **Error Handling**: User-friendly error displays with retry mechanisms
### iOS Test App
- **TimeSafari Configuration**: Test iOS community features
- **Generic Polling Interface**: Test structured request/response polling with iOS BGTaskScheduler
- **Rolling Window**: Test notification limit management
- **Endorser.ch API Integration**: Test pagination patterns
- **Background Tasks**: Validate BGTaskScheduler integration
- **Performance Metrics**: Monitor iOS-specific optimizations
- **Memory Management**: Test object pooling and cleanup
- **Enhanced UI**: Background refresh dialogs, rolling window controls, BGTaskScheduler status
- **Status Dashboard**: Real-time monitoring with iOS-specific metrics
- **Error Handling**: User-friendly error displays with retry mechanisms
### Electron Test App
- **TimeSafari Configuration**: Test Electron community features
- **Mock Implementations**: Test web platform compatibility
- **Endorser.ch API Integration**: Test API patterns
- **IPC Communication**: Validate Electron-specific APIs
- **Development Workflow**: Test plugin integration
- **Debug Information**: Platform-specific status display
- **Enhanced UI**: Service worker status, push notification setup, debug information
- **Status Dashboard**: Real-time monitoring with Electron-specific metrics
- **Error Handling**: User-friendly error displays with retry mechanisms
## Running the Test Apps
### Android
```bash
cd android-test
npm run dev # Web development server
npx cap open android # Open in Android Studio
npx cap run android # Run on device/emulator
```
#### High-Performance Emulator Launch (Linux + NVIDIA)
For optimal GPU acceleration on Linux systems:
```bash
# Launch with GPU acceleration and performance optimizations
QT_QPA_PLATFORM=xcb \
__NV_PRIME_RENDER_OFFLOAD=1 \
__GLX_VENDOR_LIBRARY_NAME=nvidia \
__VK_LAYER_NV_optimus=NVIDIA_only \
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json \
DRI_PRIME=1 \
emulator -avd TimeSafari_Emulator \
-gpu host \
-feature Vulkan \
-accel on \
-no-boot-anim \
-no-snapshot-load
```
**Benefits:**
- Hardware GPU acceleration for smoother UI
- Vulkan graphics API support
- Faster startup (no boot animation)
- Clean state (no snapshot loading)
- Optimized for NVIDIA graphics cards
#### Alternative GPU Modes
If you experience GPU binding issues, try these alternatives:
```bash
# Pure OpenGL mode (no Vulkan)
./launch-emulator-opengl.sh
# ANGLE mode (better NVIDIA compatibility)
./launch-emulator-angle.sh
# Mesa fallback (software rendering)
./launch-emulator-mesa.sh
```
**Troubleshooting:**
- See [EMULATOR_TROUBLESHOOTING.md](./EMULATOR_TROUBLESHOOTING.md) for detailed GPU binding solutions
- Check emulator banner for correct GPU binding
- Monitor GPU usage with `nvidia-smi dmon -s u`
### iOS
```bash
cd ios-test
npm run dev # Web development server
npx cap open ios # Open in Xcode
npx cap run ios # Run on device/simulator
```
### Electron
```bash
cd electron-test
npm start # Run Electron app
npm run dev # Run in development mode
```
## Testing Checklist
### Core Functionality
- [ ] TimeSafari configuration works
- [ ] Generic polling interface functions properly
- [ ] Community notification scheduling succeeds
- [ ] Endorser.ch API integration functions properly
- [ ] Error handling functions properly
- [ ] Performance metrics are accurate
### Enhanced UI Testing
- [ ] Permission management dialogs display correctly
- [ ] Settings panels save and load configuration
- [ ] Status dashboards show real-time data
- [ ] Error handling UI displays user-friendly messages
- [ ] Platform-specific features work as expected
- [ ] Responsive design works on different screen sizes
- [ ] Accessibility features function properly
### Platform-Specific
- [ ] Android exact alarm permissions, battery optimization, reboot recovery
- [ ] iOS rolling window management, background refresh, BGTaskScheduler
- [ ] Electron mock implementations, service worker, push notifications
- [ ] Cross-platform API consistency
### TimeSafari Integration
- [ ] Plugin loads without errors
- [ ] Configuration persists across sessions
- [ ] Endorser.ch API pagination works
- [ ] Community notification types process correctly
- [ ] Performance optimizations active
- [ ] Debug information accessible
## Troubleshooting
### Common Issues
1. **"Unknown command: cap"** → Install Capacitor CLI: `npm install -g @capacitor/cli`
2. **"android platform has not been added yet"** → Run `npx cap add android` first
3. **Build failures** → Check Node.js version (18+) and clear cache: `npm cache clean --force`
4. **Platform errors** → Verify platform-specific SDKs are installed
5. **API connection errors** → Ensure test API server is running on port 3001
### Quick Fixes
```bash
# Check environment
./check-environment.sh
# Reinstall dependencies
rm -rf node_modules && npm install
# Clear Capacitor cache
npx cap clean
# Re-sync platforms
npx cap sync
# Restart test API server
cd test-api && npm start
```
### Detailed Help
See [Enhanced Setup Guide](SETUP_GUIDE.md) for comprehensive troubleshooting and platform-specific solutions.
## Next Steps
1. **Run Setup Scripts**: Execute platform-specific setup
2. **Start Test API Server**: Run the TimeSafari Test API server
3. **Test Core Features**: Validate basic TimeSafari functionality
4. **Test Platform Features**: Verify platform-specific capabilities
5. **Test Endorser.ch Integration**: Validate API patterns and pagination
6. **Integration Testing**: Test with actual plugin implementation
7. **Performance Validation**: Monitor metrics and optimizations

384
test-apps/SETUP_GUIDE.md

@ -1,384 +0,0 @@
# Enhanced Test Apps Setup Guide
## Overview
This guide creates minimal Capacitor test apps for validating the Daily Notification Plugin across all target platforms with robust error handling and clear troubleshooting.
## Prerequisites
### Required Software
- **Node.js 18+**: Download from [nodejs.org](https://nodejs.org/)
- **npm**: Comes with Node.js
- **Git**: For version control
### Platform-Specific Requirements
#### Android
- **Android Studio**: Download from [developer.android.com/studio](https://developer.android.com/studio)
- **Android SDK**: Installed via Android Studio
- **Java Development Kit (JDK)**: Version 11 or higher
#### iOS (macOS only)
- **Xcode**: Install from Mac App Store
- **Xcode Command Line Tools**: `xcode-select --install`
- **iOS Simulator**: Included with Xcode
#### Electron
- **No additional requirements**: Works on any platform with Node.js
## Quick Start
### Option 1: Automated Setup (Recommended)
```bash
# Navigate to test-apps directory
cd test-apps
# Setup all platforms (run from test-apps directory)
./setup-android.sh
./setup-ios.sh
./setup-electron.sh
```
### Option 2: Manual Setup
#### Android Manual Setup
```bash
cd test-apps/android-test
# Install dependencies
npm install
# Install Capacitor CLI globally
npm install -g @capacitor/cli
# Initialize Capacitor
npx cap init "Daily Notification Android Test" "com.timesafari.dailynotification.androidtest"
# Add Android platform
npx cap add android
# Build web assets
npm run build
# Sync to native
npx cap sync android
```
#### iOS Manual Setup
```bash
cd test-apps/ios-test
# Install dependencies
npm install
# Install Capacitor CLI globally
npm install -g @capacitor/cli
# Initialize Capacitor
npx cap init "Daily Notification iOS Test" "com.timesafari.dailynotification.iostest"
# Add iOS platform
npx cap add ios
# Build web assets
npm run build
# Sync to native
npx cap sync ios
```
#### Electron Manual Setup
```bash
cd test-apps/electron-test
# Install dependencies
npm install
# Build web assets
npm run build-web
```
## Common Issues and Solutions
### Issue: "Unknown command: cap"
**Solution**: Install Capacitor CLI globally
```bash
npm install -g @capacitor/cli
```
### Issue: "android platform has not been added yet"
**Solution**: Add the Android platform first
```bash
npx cap add android
```
### Issue: "Failed to add Android platform"
**Solutions**:
1. Install Android Studio and Android SDK
2. Set `ANDROID_HOME` environment variable
3. Add Android SDK tools to your PATH
### Issue: "Failed to add iOS platform"
**Solutions**:
1. Install Xcode from Mac App Store
2. Install Xcode Command Line Tools: `xcode-select --install`
3. Ensure you're running on macOS
### Issue: Build failures
**Solutions**:
1. Check Node.js version: `node --version` (should be 18+)
2. Clear npm cache: `npm cache clean --force`
3. Delete `node_modules` and reinstall: `rm -rf node_modules && npm install`
## Platform-Specific Setup
### Android Setup Verification
```bash
# Check Android Studio installation
which studio
# Check Android SDK
echo $ANDROID_HOME
# Check Java
java -version
```
**Required Environment Variables**:
```bash
export ANDROID_HOME=/path/to/android/sdk
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
```
### iOS Setup Verification
```bash
# Check Xcode installation
xcodebuild -version
# Check iOS Simulator
xcrun simctl list devices
# Check Command Line Tools
xcode-select -p
```
### Electron Setup Verification
```bash
# Check Node.js version
node --version
# Check npm
npm --version
# Test Electron installation
npx electron --version
```
## Running the Test Apps
### Android
```bash
cd test-apps/android-test
# Web development server (for testing)
npm run dev
# Open in Android Studio
npx cap open android
# Run on device/emulator
npx cap run android
```
#### Advanced Emulator Launch (GPU Acceleration)
For optimal performance on Linux systems with NVIDIA graphics:
```bash
# Launch emulator with GPU acceleration and performance tuning
QT_QPA_PLATFORM=xcb \
__NV_PRIME_RENDER_OFFLOAD=1 \
__GLX_VENDOR_LIBRARY_NAME=nvidia \
__VK_LAYER_NV_optimus=NVIDIA_only \
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json \
DRI_PRIME=1 \
emulator -avd TimeSafari_Emulator \
-gpu host \
-feature Vulkan \
-accel on \
-cores 6 \
-memory 4096 \
-no-boot-anim \
-no-snapshot-load \
-dns-server 8.8.8.8,1.1.1.1
```
**Environment Variables Explained:**
- `QT_QPA_PLATFORM=xcb`: Use X11 backend for Qt applications
- `__NV_PRIME_RENDER_OFFLOAD=1`: Enable NVIDIA GPU offloading
- `__GLX_VENDOR_LIBRARY_NAME=nvidia`: Use NVIDIA OpenGL library
- `__VK_LAYER_NV_optimus=NVIDIA_only`: Force NVIDIA Vulkan layer
- `VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json`: Specify NVIDIA Vulkan driver
- `DRI_PRIME=1`: Enable DRI Prime for GPU selection
**Emulator Flags Explained:**
- `-gpu host`: Use host GPU for hardware acceleration
- `-feature Vulkan`: Enable Vulkan graphics API support
- `-accel on`: Enable hardware acceleration
- `-cores 6`: Allocate 6 CPU cores to emulator
- `-memory 4096`: Allocate 4GB RAM to emulator
- `-no-boot-anim`: Skip boot animation for faster startup
- `-no-snapshot-load`: Don't load snapshots for clean state
- `-dns-server 8.8.8.8,1.1.1.1`: Use explicit DNS servers
- `-feature -Bluetooth`: Disable Bluetooth to prevent hangs
#### Alternative GPU Modes
If you experience GPU binding issues (OpenGL using Intel iGPU instead of NVIDIA), try these alternative launch scripts:
```bash
# Pure OpenGL mode (no Vulkan)
./launch-emulator-opengl.sh
# ANGLE mode (better NVIDIA compatibility)
./launch-emulator-angle.sh
# Mesa fallback (software rendering)
./launch-emulator-mesa.sh
```
**Troubleshooting GPU Issues:**
- Check emulator banner for `OpenGL Vendor=Google (NVIDIA)`
- Monitor GPU usage: `nvidia-smi dmon -s u`
- See [EMULATOR_TROUBLESHOOTING.md](./EMULATOR_TROUBLESHOOTING.md) for detailed solutions
### iOS
```bash
cd test-apps/ios-test
# Web development server (for testing)
npm run dev
# Open in Xcode
npx cap open ios
# Run on device/simulator
npx cap run ios
```
### Electron
```bash
cd test-apps/electron-test
# Run Electron app
npm start
# Run in development mode
npm run dev
# Build and run
npm run electron
```
## Testing Workflow
### 1. Web Testing (Recommended First)
```bash
# Test each platform's web version first
cd test-apps/android-test && npm run dev
cd test-apps/ios-test && npm run dev
cd test-apps/electron-test && npm start
```
### 2. Native Testing
```bash
# After web testing succeeds, test native platforms
cd test-apps/android-test && npx cap run android
cd test-apps/ios-test && npx cap run ios
```
### 3. Integration Testing
- Test plugin configuration
- Test notification scheduling
- Test platform-specific features
- Test error handling
- Test performance metrics
## Troubleshooting Checklist
### General Issues
- [ ] Node.js 18+ installed
- [ ] npm working correctly
- [ ] Capacitor CLI installed globally
- [ ] Dependencies installed (`npm install`)
### Android Issues
- [ ] Android Studio installed
- [ ] Android SDK configured
- [ ] `ANDROID_HOME` environment variable set
- [ ] Java JDK 11+ installed
- [ ] Android platform added (`npx cap add android`)
### iOS Issues
- [ ] Xcode installed (macOS only)
- [ ] Xcode Command Line Tools installed
- [ ] iOS Simulator available
- [ ] iOS platform added (`npx cap add ios`)
### Electron Issues
- [ ] Node.js working correctly
- [ ] Dependencies installed
- [ ] Web assets built (`npm run build-web`)
## Development Tips
### Web Development
- Use `npm run dev` for hot reloading
- Test plugin APIs in browser console
- Use browser dev tools for debugging
### Native Development
- Use `npx cap sync` after making changes
- Check native logs for detailed errors
- Test on both physical devices and simulators
### Debugging
- Check console logs for errors
- Use `npx cap doctor` to diagnose issues
- Verify platform-specific requirements
## Next Steps
1. **Run Setup Scripts**: Execute platform-specific setup
2. **Test Web Versions**: Validate basic functionality
3. **Test Native Platforms**: Verify platform-specific features
4. **Integration Testing**: Test with actual plugin implementation
5. **Performance Validation**: Monitor metrics and optimizations
## Support
If you encounter issues not covered in this guide:
1. Check the [Capacitor Documentation](https://capacitorjs.com/docs)
2. Verify platform-specific requirements
3. Check console logs for detailed error messages
4. Ensure all prerequisites are properly installed

20
test-apps/android-test/.eslintrc.cjs

@ -1,20 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

105
test-apps/android-test/.gitignore

@ -1,105 +0,0 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
*.tsbuildinfo
# Capacitor
android/
ios/
.capacitor/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.local
.env.development.local
.env.test.local
.env.production.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

160
test-apps/android-test/GRADLE_TROUBLESHOOTING.md

@ -1,160 +0,0 @@
# Android Test App Gradle Sync Troubleshooting
## Problem: Gradle Sync Failure
**Error Message:**
```
Unable to find method 'org.gradle.api.artifacts.Dependency org.gradle.api.artifacts.dsl.DependencyHandler.module(java.lang.Object)'
```
## Root Cause
The Android test app was using **Gradle 9.0-milestone-1** (pre-release) with **Android Gradle Plugin 8.0.0**, causing version incompatibility issues.
## Solution Applied
### 1. Updated Gradle Version
**File:** `android/gradle/wrapper/gradle-wrapper.properties`
```properties
# Changed from:
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
# To:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
```
### 2. Updated Android Gradle Plugin
**File:** `android/build.gradle`
```gradle
// Changed from:
classpath 'com.android.tools.build:gradle:8.0.0'
// To:
classpath 'com.android.tools.build:gradle:8.13.0'
```
### 3. Updated Google Services Plugin
```gradle
// Changed from:
classpath 'com.google.gms:google-services:4.3.15'
// To:
classpath 'com.google.gms:google-services:4.4.0'
```
### 4. Updated AndroidX Dependencies
**File:** `android/variables.gradle`
```gradle
// Updated to latest stable versions:
androidxAppCompatVersion = '1.7.1' // was 1.6.1
androidxActivityVersion = '1.8.2' // was 1.7.0
androidxCoreVersion = '1.12.0' // was 1.10.0
androidxFragmentVersion = '1.6.2' // was 1.5.6
coreSplashScreenVersion = '1.0.1' // was 1.0.0
androidxWebkitVersion = '1.8.0' // was 1.6.1
compileSdkVersion = 34 // was 33
targetSdkVersion = 34 // was 33
```
## Manual Fix Steps
If you encounter this issue again:
### Step 1: Clean Gradle Cache
```bash
cd test-apps/android-test/android
./gradlew clean
./gradlew --stop
```
### Step 2: Clear Gradle Wrapper Cache
```bash
rm -rf ~/.gradle/wrapper/dists/gradle-9.0-milestone-1*
```
### Step 3: Re-sync Project
In Android Studio:
1. Click **File** → **Sync Project with Gradle Files**
2. Or click the **Sync Now** link in the error banner
### Step 4: If Still Failing
```bash
# Delete all Gradle caches
rm -rf ~/.gradle/caches
rm -rf ~/.gradle/wrapper
# Re-download Gradle
cd test-apps/android-test/android
./gradlew wrapper --gradle-version 8.4
```
## Prevention
### Use Stable Versions
Always use stable, tested version combinations:
| Android Gradle Plugin | Gradle Version | Status |
|----------------------|----------------|---------|
| 8.13.0 | 8.13 | ✅ Latest Stable |
| 8.1.4 | 8.4 | ✅ Stable |
| 8.0.0 | 8.0 | ✅ Stable |
| 7.4.2 | 7.5 | ✅ Stable |
| 8.0.0 | 9.0-milestone-1 | ❌ Incompatible |
### Version Compatibility Check
- **Android Gradle Plugin 8.13.0** requires **Gradle 8.0+**
- **Gradle 8.13** is the latest stable version
- **AndroidX AppCompat 1.7.1** is the latest stable version
- Avoid pre-release versions in production
## Additional Troubleshooting
### If Sync Still Fails
1. **Check Java Version**
```bash
java -version
# Should be Java 17+ for AGP 8.1.4
```
2. **Check Android SDK**
```bash
echo $ANDROID_HOME
# Should point to Android SDK location
```
3. **Check Local Properties**
```bash
# Verify android/local.properties exists
cat test-apps/android-test/android/local.properties
```
4. **Recreate Project**
```bash
cd test-apps/android-test
rm -rf android/
npx cap add android
```
## Success Indicators
After applying the fix, you should see:
- ✅ **Gradle sync successful**
- ✅ **No red error banners**
- ✅ **Build.gradle file opens without errors**
- ✅ **Project structure loads correctly**
## Next Steps
Once Gradle sync is successful:
1. **Build the project**: `./gradlew build`
2. **Run on device**: `npx cap run android`
3. **Test plugin functionality**: Use the test API server
4. **Validate notifications**: Test the Daily Notification Plugin
## Related Issues
- **Build failures**: Usually resolved by Gradle sync fix
- **Plugin not found**: Check Capacitor plugin installation
- **Permission errors**: Verify Android manifest permissions
- **Runtime crashes**: Check plugin initialization code

329
test-apps/android-test/README.md

@ -1,329 +0,0 @@
# Daily Notification Test App - Vue 3
A modern Vue 3 + Vite + Capacitor test application for the Daily Notification Plugin, built with vue-facing-decorator for TypeScript class-based components.
## 🚀 Features
- **Vue 3** with Composition API and TypeScript
- **Vite** for fast development and building
- **vue-facing-decorator** for class-based components
- **Pinia** for state management
- **Vue Router** for navigation
- **Capacitor** for native Android functionality
- **Modern UI** with glassmorphism design
- **Responsive** design for mobile and desktop
## 📱 App Structure
```
src/
├── components/ # Reusable Vue components
│ ├── cards/ # Card components (ActionCard, StatusCard, etc.)
│ ├── items/ # List item components
│ ├── layout/ # Layout components (Header, Footer)
│ └── ui/ # UI components (Loading, Error, etc.)
├── stores/ # Pinia stores
│ ├── app.ts # Global app state
│ └── notifications.ts # Notification management
├── views/ # Page components
│ ├── HomeView.vue # Dashboard
│ ├── ScheduleView.vue # Schedule notifications
│ ├── NotificationsView.vue # Manage notifications
│ ├── StatusView.vue # System status
│ ├── HistoryView.vue # Notification history
│ └── SettingsView.vue # App settings
├── router/ # Vue Router configuration
├── types/ # TypeScript type definitions
└── utils/ # Utility functions
```
## 🛠️ Development
### Prerequisites
- Node.js 18+
- npm or yarn
- Android Studio (for Android development)
- Capacitor CLI
### Installation
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Type checking
npm run type-check
# Linting
npm run lint
```
### Android Development
```bash
# Sync with Capacitor
npm run sync
# Open Android Studio
npm run open
# Run on Android device/emulator
npm run android
```
## 🎨 UI Components
### Class-Based Components
All components use vue-facing-decorator for TypeScript class-based syntax:
```typescript
@Component({
components: {
ChildComponent
}
})
export default class MyComponent extends Vue {
@Prop() title!: string
private data = ref('')
get computedValue(): string {
return this.data.value.toUpperCase()
}
private handleClick(): void {
// Handle click
}
}
```
### State Management
Uses Pinia stores for reactive state management:
```typescript
// stores/notifications.ts
export const useNotificationsStore = defineStore('notifications', () => {
const scheduledNotifications = ref<ScheduledNotification[]>([])
async function scheduleNotification(options: ScheduleOptions): Promise<void> {
// Schedule logic
}
return {
scheduledNotifications,
scheduleNotification
}
})
```
## 📊 Features
### Dashboard (Home)
- Quick action cards
- System status overview
- Next scheduled notification
- Recent activity feed
### Schedule Notifications
- Time picker
- Title and message inputs
- Sound and priority options
- URL support for deep linking
- Quick schedule presets
### Notification Management
- View all scheduled notifications
- Cancel notifications
- Status indicators
### System Status
- Permission checks
- Channel status
- Exact alarm settings
- Platform information
- Test notification functionality
### History
- Delivered notification history
- Click and dismiss tracking
- Time-based filtering
## 🔧 Configuration
### Capacitor Config
```typescript
// capacitor.config.ts
const config: CapacitorConfig = {
appId: 'com.timesafari.dailynotification.androidtest',
appName: 'Daily Notification Test - Vue 3',
webDir: 'dist',
plugins: {
DailyNotification: {
storage: 'shared',
ttlSeconds: 1800,
prefetchLeadMinutes: 15,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true
}
}
}
```
### Vite Config
```typescript
// vite.config.ts
export default defineConfig({
plugins: [vue()],
build: {
outDir: 'dist',
sourcemap: true
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
```
## 🎯 Testing the Plugin
1. **Start Development Server**
```bash
npm run dev
```
2. **Build and Sync**
```bash
npm run build
npm run sync
```
3. **Run on Android**
```bash
npm run android
```
4. **Test Features**
- Schedule notifications
- Check system status
- View notification history
- Test clickable notifications
## 📱 Native Features
- **Notification Scheduling** - Schedule daily notifications
- **Permission Management** - Check and request permissions
- **Status Monitoring** - Real-time system status
- **Deep Linking** - URL support in notifications
- **Background Processing** - WorkManager integration
## 🎨 Design System
### Colors
- Primary: Linear gradient (purple to blue)
- Success: #4caf50
- Warning: #ff9800
- Error: #f44336
- Info: #2196f3
### Typography
- Headers: Bold, white with text-shadow
- Body: Regular, rgba white
- Code: Courier New monospace
### Components
- Glassmorphism design with backdrop-filter
- Rounded corners (8px, 12px, 16px)
- Smooth transitions and hover effects
- Responsive grid layouts
## 🚀 Production Build
```bash
# Build for production
npm run build
# Preview production build
npm run preview
# Sync with Capacitor
npm run sync
# Build Android APK
npm run android
```
## 📝 Scripts
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run preview` - Preview production build
- `npm run android` - Run on Android
- `npm run sync` - Sync with Capacitor
- `npm run open` - Open Android Studio
- `npm run lint` - Run ESLint
- `npm run type-check` - TypeScript type checking
## 🔍 Debugging
### Vue DevTools
Install Vue DevTools browser extension for component inspection.
### Capacitor Logs
```bash
# Android logs
adb logcat | grep -i "daily\|notification"
# Capacitor logs
npx cap run android --livereload --external
```
### TypeScript
Enable strict mode in `tsconfig.json` for better type checking.
## 📚 Dependencies
### Core
- Vue 3.4+
- Vite 5.0+
- TypeScript 5.3+
- Capacitor 5.0+
### UI & State
- vue-facing-decorator 3.0+
- Pinia 2.1+
- Vue Router 4.2+
### Development
- ESLint + TypeScript configs
- Vue TSC for type checking
- Modern module resolution
## 🤝 Contributing
1. Follow Vue 3 + TypeScript best practices
2. Use vue-facing-decorator for class components
3. Maintain responsive design
4. Add proper TypeScript types
5. Test on both web and Android
## 📄 License
MIT License - see LICENSE file for details.
---
**Built with ❤️ using Vue 3 + Vite + Capacitor**

25
test-apps/android-test/capacitor.config.ts

@ -1,25 +0,0 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.timesafari.dailynotification.androidtest',
appName: 'Daily Notification Test - Vue 3',
webDir: 'dist',
server: {
androidScheme: 'https'
},
plugins: {
DailyNotification: {
storage: 'shared',
ttlSeconds: 1800,
prefetchLeadMinutes: 15,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true
}
},
android: {
allowMixedContent: true
}
};
export default config;

90
test-apps/android-test/env.d.ts

@ -1,90 +0,0 @@
/**
* Environment Type Declarations
*
* @author Matthew Raymer
* @version 1.0.0
*/
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// Capacitor plugin declarations
declare global {
interface Window {
DailyNotification: {
scheduleDailyNotification: (options: {
time: string
title?: string
body?: string
sound?: boolean
priority?: string
url?: string
}) => Promise<void>
scheduleDailyReminder: (options: {
id: string
title: string
body: string
time: string
sound?: boolean
vibration?: boolean
priority?: string
repeatDaily?: boolean
timezone?: string
}) => Promise<void>
cancelDailyReminder: (options: {
reminderId: string
}) => Promise<void>
updateDailyReminder: (options: {
reminderId: string
title?: string
body?: string
time?: string
sound?: boolean
vibration?: boolean
priority?: string
repeatDaily?: boolean
timezone?: string
}) => Promise<void>
getLastNotification: () => Promise<{
id: string
title: string
body: string
scheduledTime: number
deliveredAt: number
}>
checkStatus: () => Promise<{
canScheduleNow: boolean
postNotificationsGranted: boolean
channelEnabled: boolean
channelImportance: number
channelId: string
exactAlarmsGranted: boolean
exactAlarmsSupported: boolean
androidVersion: number
nextScheduledAt: number
}>
checkChannelStatus: () => Promise<{
enabled: boolean
importance: number
id: string
}>
openChannelSettings: () => Promise<void>
openExactAlarmSettings: () => Promise<void>
}
}
}
export {}

73
test-apps/android-test/index.html

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Daily Notification Test - Vue 3</title>
<!-- Capacitor meta tags -->
<meta name="color-scheme" content="light dark" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<!-- PWA meta tags -->
<meta name="theme-color" content="#1976d2" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Daily Notification Test" />
<!-- Styles -->
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
#app {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Loading spinner */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
font-size: 18px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="app">
<div class="loading">
<div class="spinner"></div>
Loading Daily Notification Test App...
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

42
test-apps/android-test/package.json

@ -1,42 +0,0 @@
{
"name": "daily-notification-android-test",
"version": "1.0.0",
"description": "Vue 3 + Vite + Capacitor test app for Daily Notification Plugin",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"android": "npx cap run android",
"sync": "npx cap sync android",
"open": "npx cap open android",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"type-check": "vue-tsc --noEmit"
},
"keywords": ["capacitor", "android", "notifications", "test", "vue3", "vite", "typescript"],
"author": "Matthew Raymer",
"license": "MIT",
"dependencies": {
"@capacitor/core": "^5.0.0",
"@capacitor/android": "^5.0.0",
"@capacitor/cli": "^5.0.0",
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"vue-facing-decorator": "^3.0.0"
},
"devDependencies": {
"@capacitor/cli": "^5.0.0",
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.0",
"eslint": "^8.0.0",
"eslint-plugin-vue": "^9.0.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}

147
test-apps/android-test/src/App.vue

@ -1,147 +0,0 @@
<!--
/**
* Main App Component
*
* Vue 3 + vue-facing-decorator + TypeScript
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div id="app" class="app-container">
<!-- Header -->
<AppHeader />
<!-- Main Content -->
<main class="main-content">
<router-view />
</main>
<!-- Footer -->
<AppFooter />
<!-- Global Loading Overlay -->
<LoadingOverlay v-if="isLoading" />
<!-- Global Error Dialog -->
<ErrorDialog
v-if="errorMessage"
:message="errorMessage"
@close="clearError"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { Capacitor } from '@capacitor/core'
import AppHeader from '@/components/layout/AppHeader.vue'
import AppFooter from '@/components/layout/AppFooter.vue'
import LoadingOverlay from '@/components/ui/LoadingOverlay.vue'
import ErrorDialog from '@/components/ui/ErrorDialog.vue'
import { useAppStore } from '@/stores/app'
@Component({
components: {
AppHeader,
AppFooter,
LoadingOverlay,
ErrorDialog
}
})
export default class App extends Vue {
private appStore = useAppStore()
get isLoading(): boolean {
return this.appStore.isLoading
}
get errorMessage(): string | null {
return this.appStore.errorMessage
}
mounted(): void {
this.initializeApp()
}
private async initializeApp(): Promise<void> {
try {
this.appStore.setLoading(true)
// Initialize Capacitor plugins
await this.initializeCapacitor()
// Check notification permissions
await this.checkNotificationPermissions()
console.log('✅ App initialized successfully')
} catch (error) {
console.error('❌ App initialization failed:', error)
this.appStore.setError('Failed to initialize app: ' + (error as Error).message)
} finally {
this.appStore.setLoading(false)
}
}
private async initializeCapacitor(): Promise<void> {
if (Capacitor.isNativePlatform()) {
console.log('📱 Initializing Capacitor for native platform')
// Check if DailyNotification plugin is available
if (!window.DailyNotification) {
console.warn('⚠️ DailyNotification plugin not loaded - buttons will show error messages')
this.appStore.setError('DailyNotification plugin not loaded. Please restart the app to enable full functionality.')
return
}
console.log('✅ DailyNotification plugin available')
} else {
console.log('🌐 Running in web mode - DailyNotification plugin not available')
}
}
private async checkNotificationPermissions(): Promise<void> {
if (Capacitor.isNativePlatform() && window.DailyNotification) {
try {
const status = await window.DailyNotification.checkStatus()
console.log('📊 Notification Status:', status)
if (!status.canScheduleNow) {
console.warn('⚠️ Notifications not fully configured')
}
} catch (error) {
console.warn('⚠️ Could not check notification status:', error)
}
}
}
clearError(): void {
this.appStore.clearError()
}
}
</script>
<style scoped>
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.main-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
/* Responsive design */
@media (max-width: 768px) {
.main-content {
padding: 8px;
}
}
</style>

163
test-apps/android-test/src/components/cards/ActionCard.vue

@ -1,163 +0,0 @@
<!--
/**
* Action Card Component
*
* Reusable card component for quick actions
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div
class="action-card"
:class="{ 'loading': loading, 'disabled': disabled }"
@click="handleClick"
>
<div class="card-content">
<div class="icon-container">
<span class="icon">{{ icon }}</span>
</div>
<div class="text-content">
<h3 class="title">{{ title }}</h3>
<p class="description">{{ description }}</p>
</div>
<div class="action-indicator">
<span v-if="loading" class="spinner"></span>
<span v-else class="arrow"></span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
@Component
export default class ActionCard extends Vue {
@Prop({ required: true }) icon!: string
@Prop({ required: true }) title!: string
@Prop({ required: true }) description!: string
@Prop({ default: false }) loading!: boolean
@Prop({ default: false }) disabled!: boolean
handleClick(): void {
if (!this.loading && !this.disabled) {
this.$emit('click')
}
}
}
</script>
<style scoped>
.action-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.action-card:hover:not(.disabled) {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.action-card:active:not(.disabled) {
transform: translateY(0);
}
.action-card.loading {
cursor: wait;
opacity: 0.7;
}
.action-card.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.card-content {
display: flex;
align-items: center;
gap: 16px;
}
.icon-container {
flex-shrink: 0;
}
.icon {
font-size: 2rem;
display: block;
}
.text-content {
flex: 1;
min-width: 0;
}
.title {
font-size: 1.1rem;
font-weight: 600;
color: white;
margin: 0 0 4px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.description {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
margin: 0;
line-height: 1.4;
}
.action-indicator {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.7);
font-size: 1.2rem;
font-weight: bold;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.action-card {
padding: 16px;
}
.card-content {
gap: 12px;
}
.icon {
font-size: 1.5rem;
}
.title {
font-size: 1rem;
}
.description {
font-size: 0.85rem;
}
}
</style>

95
test-apps/android-test/src/components/cards/InfoCard.vue

@ -1,95 +0,0 @@
<!--
/**
* Info Card Component
*
* Simple information display card
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="info-card">
<div class="info-header">
<span class="info-icon">{{ icon }}</span>
<h3 class="info-title">{{ title }}</h3>
</div>
<div class="info-content">
<p class="info-value">{{ value }}</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
@Component
export default class InfoCard extends Vue {
@Prop({ required: true }) title!: string
@Prop({ required: true }) value!: string
@Prop({ required: true }) icon!: string
}
</script>
<style scoped>
.info-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.info-card:hover {
background: rgba(255, 255, 255, 0.15);
}
.info-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.info-icon {
font-size: 1.2rem;
}
.info-title {
font-size: 0.9rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.info-content {
margin: 0;
}
.info-value {
font-size: 1rem;
font-weight: 500;
color: white;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
word-break: break-word;
}
/* Responsive design */
@media (max-width: 768px) {
.info-card {
padding: 12px;
}
.info-title {
font-size: 0.85rem;
}
.info-value {
font-size: 0.9rem;
}
}
</style>

181
test-apps/android-test/src/components/cards/NotificationCard.vue

@ -1,181 +0,0 @@
<!--
/**
* Notification Card Component
*
* Displays notification information
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="notification-card">
<div class="notification-header">
<h3 class="notification-title">{{ notification.title }}</h3>
<div class="notification-status" :class="statusClass">
{{ statusText }}
</div>
</div>
<div class="notification-content">
<p class="notification-body">{{ notification.body }}</p>
</div>
<div class="notification-footer">
<div class="notification-time">
<span class="time-label">Scheduled:</span>
<span class="time-value">{{ formatScheduledTime }}</span>
</div>
<div v-if="notification.deliveredAt" class="notification-delivered">
<span class="delivered-label">Delivered:</span>
<span class="delivered-value">{{ formatDeliveredTime }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
import type { ScheduledNotification } from '@/stores/notifications'
@Component
export default class NotificationCard extends Vue {
@Prop({ required: true }) notification!: ScheduledNotification
get statusClass(): string {
return `status-${this.notification.status}`
}
get statusText(): string {
const statusMap = {
scheduled: 'Scheduled',
delivered: 'Delivered',
cancelled: 'Cancelled'
}
return statusMap[this.notification.status]
}
get formatScheduledTime(): string {
const date = new Date(this.notification.scheduledTime)
return date.toLocaleString()
}
get formatDeliveredTime(): string {
if (!this.notification.deliveredAt) return 'Not delivered'
const date = new Date(this.notification.deliveredAt)
return date.toLocaleString()
}
}
</script>
<style scoped>
.notification-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.notification-title {
font-size: 1.1rem;
font-weight: 600;
color: white;
margin: 0;
flex: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.notification-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-scheduled {
background: rgba(33, 150, 243, 0.2);
color: #2196f3;
border: 1px solid rgba(33, 150, 243, 0.3);
}
.status-delivered {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.status-cancelled {
background: rgba(158, 158, 158, 0.2);
color: #9e9e9e;
border: 1px solid rgba(158, 158, 158, 0.3);
}
.notification-content {
margin-bottom: 16px;
}
.notification-body {
color: rgba(255, 255, 255, 0.9);
font-size: 0.95rem;
line-height: 1.5;
margin: 0;
}
.notification-footer {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.notification-time,
.notification-delivered {
display: flex;
justify-content: space-between;
align-items: center;
}
.time-label,
.delivered-label {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
.time-value,
.delivered-value {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.9);
font-family: 'Courier New', monospace;
}
/* Responsive design */
@media (max-width: 768px) {
.notification-card {
padding: 12px;
}
.notification-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.notification-footer {
gap: 6px;
}
}
</style>

264
test-apps/android-test/src/components/cards/StatusCard.vue

@ -1,264 +0,0 @@
<!--
/**
* Status Card Component
*
* Displays notification system status with visual indicators
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="status-card">
<div class="status-header">
<h3 class="status-title">System Status</h3>
<div class="status-indicator" :class="statusClass">
<span class="status-dot"></span>
<span class="status-text">{{ statusText }}</span>
</div>
</div>
<div v-if="status" class="status-details">
<div class="status-grid">
<StatusItem
label="Notifications"
:value="status.postNotificationsGranted ? 'Granted' : 'Denied'"
:status="status.postNotificationsGranted ? 'success' : 'error'"
/>
<StatusItem
label="Channel"
:value="channelStatusText"
:status="status.channelEnabled ? 'success' : 'warning'"
/>
<StatusItem
label="Exact Alarms"
:value="status.exactAlarmsGranted ? 'Granted' : 'Denied'"
:status="status.exactAlarmsGranted ? 'success' : 'error'"
/>
<StatusItem
label="Android Version"
:value="`API ${status.androidVersion}`"
status="info"
/>
</div>
<div v-if="status.nextScheduledAt > 0" class="next-scheduled">
<h4 class="next-title">Next Scheduled</h4>
<p class="next-time">{{ formatNextScheduledTime }}</p>
</div>
</div>
<div v-else class="no-status">
<p class="no-status-text">Status not available</p>
<button class="refresh-button" @click="refreshStatus">
🔄 Refresh
</button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
import type { NotificationStatus } from '@/stores/app'
import StatusItem from '@/components/items/StatusItem.vue'
@Component({
components: {
StatusItem
}
})
export default class StatusCard extends Vue {
@Prop() status!: NotificationStatus | null
get statusClass(): string {
if (!this.status) return 'unknown'
if (this.status.canScheduleNow) return 'ready'
return 'not-ready'
}
get statusText(): string {
if (!this.status) return 'Unknown'
if (this.status.canScheduleNow) return 'Ready'
return 'Not Ready'
}
get channelStatusText(): string {
if (!this.status) return 'Unknown'
if (!this.status.channelEnabled) return 'Disabled'
const importanceMap: Record<number, string> = {
0: 'None',
1: 'Min',
2: 'Low',
3: 'Default',
4: 'High',
5: 'Max'
}
return importanceMap[this.status.channelImportance] || 'Unknown'
}
get formatNextScheduledTime(): string {
if (!this.status || this.status.nextScheduledAt <= 0) {
return 'No notifications scheduled'
}
const date = new Date(this.status.nextScheduledAt)
const now = new Date()
const diffMs = date.getTime() - now.getTime()
if (diffMs < 0) {
return 'Past due'
}
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
if (diffHours > 24) {
const diffDays = Math.floor(diffHours / 24)
return `In ${diffDays} day${diffDays > 1 ? 's' : ''}`
} else if (diffHours > 0) {
return `In ${diffHours}h ${diffMinutes}m`
} else {
return `In ${diffMinutes} minutes`
}
}
refreshStatus(): void {
this.$emit('refresh')
}
}
</script>
<style scoped>
.status-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.status-title {
font-size: 1.2rem;
font-weight: 600;
color: white;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.status-indicator.ready {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.status-indicator.not-ready {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
border: 1px solid rgba(244, 67, 54, 0.3);
}
.status-indicator.unknown {
background: rgba(158, 158, 158, 0.2);
color: #9e9e9e;
border: 1px solid rgba(158, 158, 158, 0.3);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-details {
space-y: 16px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.next-scheduled {
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.next-title {
font-size: 1rem;
font-weight: 600;
color: white;
margin: 0 0 8px 0;
}
.next-time {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
margin: 0;
font-family: 'Courier New', monospace;
}
.no-status {
text-align: center;
padding: 20px;
}
.no-status-text {
color: rgba(255, 255, 255, 0.7);
margin: 0 0 16px 0;
}
.refresh-button {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.refresh-button:hover {
background: rgba(255, 255, 255, 0.15);
}
/* Responsive design */
@media (max-width: 768px) {
.status-card {
padding: 16px;
}
.status-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.status-grid {
grid-template-columns: 1fr;
}
}
</style>

141
test-apps/android-test/src/components/items/ActivityItem.vue

@ -1,141 +0,0 @@
<!--
/**
* Activity Item Component
*
* Individual activity history item
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="activity-item">
<div class="activity-icon">
<span>{{ activityIcon }}</span>
</div>
<div class="activity-content">
<div class="activity-title">{{ activity.title }}</div>
<div class="activity-time">{{ formatActivityTime }}</div>
</div>
<div class="activity-status" :class="statusClass">
<span class="status-dot"></span>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
import type { NotificationHistory } from '@/stores/notifications'
@Component
export default class ActivityItem extends Vue {
@Prop({ required: true }) activity!: NotificationHistory
get activityIcon(): string {
if (this.activity.clicked) return '👆'
if (this.activity.dismissed) return '❌'
return '📱'
}
get statusClass(): string {
if (this.activity.clicked) return 'clicked'
if (this.activity.dismissed) return 'dismissed'
return 'delivered'
}
get formatActivityTime(): string {
const date = new Date(this.activity.deliveredAt)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
if (diffMs < 60000) { // Less than 1 minute
return 'Just now'
} else if (diffMs < 3600000) { // Less than 1 hour
const minutes = Math.floor(diffMs / 60000)
return `${minutes}m ago`
} else if (diffMs < 86400000) { // Less than 1 day
const hours = Math.floor(diffMs / 3600000)
return `${hours}h ago`
} else {
return date.toLocaleDateString()
}
}
}
</script>
<style scoped>
.activity-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.activity-item:hover {
background: rgba(255, 255, 255, 0.08);
}
.activity-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
font-size: 1.2rem;
}
.activity-content {
flex: 1;
min-width: 0;
}
.activity-title {
font-size: 0.9rem;
font-weight: 500;
color: white;
margin: 0 0 4px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-time {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
.activity-status {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-clicked .status-dot {
background: #4caf50;
}
.status-dismissed .status-dot {
background: #f44336;
}
.status-delivered .status-dot {
background: #2196f3;
}
</style>

170
test-apps/android-test/src/components/items/HistoryItem.vue

@ -1,170 +0,0 @@
<!--
/**
* History Item Component
*
* Individual notification history item
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="history-item">
<div class="history-icon">
<span>{{ historyIcon }}</span>
</div>
<div class="history-content">
<div class="history-title">{{ history.title }}</div>
<div class="history-body">{{ history.body }}</div>
<div class="history-time">{{ formatHistoryTime }}</div>
</div>
<div class="history-actions">
<span class="action-indicator" :class="actionClass">
{{ actionText }}
</span>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
import type { NotificationHistory } from '@/stores/notifications'
@Component
export default class HistoryItem extends Vue {
@Prop({ required: true }) history!: NotificationHistory
get historyIcon(): string {
if (this.history.clicked) return '👆'
if (this.history.dismissed) return '❌'
return '📱'
}
get actionClass(): string {
if (this.history.clicked) return 'clicked'
if (this.history.dismissed) return 'dismissed'
return 'delivered'
}
get actionText(): string {
if (this.history.clicked) return 'Clicked'
if (this.history.dismissed) return 'Dismissed'
return 'Delivered'
}
get formatHistoryTime(): string {
const date = new Date(this.history.deliveredAt)
return date.toLocaleString()
}
}
</script>
<style scoped>
.history-item {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.15);
}
.history-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
font-size: 1.5rem;
}
.history-content {
flex: 1;
min-width: 0;
}
.history-title {
font-size: 1rem;
font-weight: 600;
color: white;
margin: 0 0 4px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.history-body {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 8px 0;
line-height: 1.4;
}
.history-time {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.6);
font-family: 'Courier New', monospace;
}
.history-actions {
flex-shrink: 0;
}
.action-indicator {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.action-indicator.clicked {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.action-indicator.dismissed {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
border: 1px solid rgba(244, 67, 54, 0.3);
}
.action-indicator.delivered {
background: rgba(33, 150, 243, 0.2);
color: #2196f3;
border: 1px solid rgba(33, 150, 243, 0.3);
}
/* Responsive design */
@media (max-width: 768px) {
.history-item {
padding: 12px;
gap: 12px;
}
.history-icon {
width: 32px;
height: 32px;
font-size: 1.2rem;
}
.history-title {
font-size: 0.9rem;
}
.history-body {
font-size: 0.85rem;
}
}
</style>

99
test-apps/android-test/src/components/items/StatusItem.vue

@ -1,99 +0,0 @@
<!--
/**
* Status Item Component
*
* Individual status display item
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="status-item">
<div class="status-label">{{ label }}</div>
<div class="status-value" :class="statusClass">
<span class="status-dot"></span>
{{ value }}
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
@Component
export default class StatusItem extends Vue {
@Prop({ required: true }) label!: string
@Prop({ required: true }) value!: string
@Prop({ default: 'info' }) status!: 'success' | 'warning' | 'error' | 'info'
get statusClass(): string {
return `status-${this.status}`
}
}
</script>
<style scoped>
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-label {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.status-value {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-success {
color: #4caf50;
}
.status-success .status-dot {
background: #4caf50;
}
.status-warning {
color: #ff9800;
}
.status-warning .status-dot {
background: #ff9800;
}
.status-error {
color: #f44336;
}
.status-error .status-dot {
background: #f44336;
}
.status-info {
color: #2196f3;
}
.status-info .status-dot {
background: #2196f3;
}
</style>

75
test-apps/android-test/src/components/layout/AppFooter.vue

@ -1,75 +0,0 @@
<!--
/**
* App Footer Component
*
* Simple footer with app information
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<footer class="app-footer">
<div class="footer-content">
<div class="footer-left">
<p class="footer-text">
Daily Notification Test App v1.0.0
</p>
</div>
<div class="footer-right">
<p class="footer-text">
Built with Vue 3 + Vite + Capacitor
</p>
</div>
</div>
</footer>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class AppFooter extends Vue {}
</script>
<style scoped>
.app-footer {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 16px 20px;
margin-top: auto;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.footer-text {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
/* Responsive design */
@media (max-width: 768px) {
.app-footer {
padding: 12px 16px;
}
.footer-content {
flex-direction: column;
gap: 8px;
text-align: center;
}
.footer-text {
font-size: 0.8rem;
}
}
</style>

239
test-apps/android-test/src/components/layout/AppHeader.vue

@ -1,239 +0,0 @@
<!--
/**
* App Header Component
*
* Navigation header with menu and status indicators
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<header class="app-header">
<div class="header-content">
<!-- Logo and Title -->
<div class="header-left">
<div class="logo">
<span class="logo-icon">🔔</span>
<span class="logo-text">Daily Notification Test</span>
</div>
</div>
<!-- Navigation -->
<nav class="header-nav">
<router-link
v-for="item in navigationItems"
:key="item.name"
:to="item.path"
class="nav-item"
:class="{ 'active': $route.name === item.name }"
>
<span class="nav-icon">{{ item.icon }}</span>
<span class="nav-text">{{ item.label }}</span>
</router-link>
</nav>
<!-- Status Indicator -->
<div class="header-right">
<div class="status-indicator" :class="statusClass">
<span class="status-dot"></span>
</div>
</div>
</div>
</header>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { useAppStore } from '@/stores/app'
interface NavigationItem {
name: string
path: string
label: string
icon: string
}
@Component
export default class AppHeader extends Vue {
private appStore = useAppStore()
navigationItems: NavigationItem[] = [
{ name: 'Home', path: '/', label: 'Home', icon: '🏠' },
{ name: 'Schedule', path: '/schedule', label: 'Schedule', icon: '📅' },
{ name: 'Notifications', path: '/notifications', label: 'Notifications', icon: '📱' },
{ name: 'Status', path: '/status', label: 'Status', icon: '📊' },
{ name: 'History', path: '/history', label: 'History', icon: '📋' },
{ name: 'Logs', path: '/logs', label: 'Logs', icon: '📜' }
]
get statusClass(): string {
const status = this.appStore.notificationStatus
if (!status) return 'unknown'
if (status.canScheduleNow) return 'ready'
return 'not-ready'
}
}
</script>
<style scoped>
.app-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding: 12px 20px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
}
.header-left {
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
}
.logo-icon {
font-size: 1.5rem;
}
.logo-text {
font-size: 1.1rem;
font-weight: 600;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.header-nav {
display: flex;
gap: 8px;
flex: 1;
justify-content: center;
}
.nav-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 8px;
text-decoration: none;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.nav-item.active {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.nav-icon {
font-size: 1rem;
}
.nav-text {
display: none;
}
.header-right {
flex-shrink: 0;
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 16px;
font-size: 0.8rem;
}
.status-indicator.ready {
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.status-indicator.not-ready {
background: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.3);
}
.status-indicator.unknown {
background: rgba(158, 158, 158, 0.2);
border: 1px solid rgba(158, 158, 158, 0.3);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-indicator.ready .status-dot {
background: #4caf50;
}
.status-indicator.not-ready .status-dot {
background: #f44336;
}
.status-indicator.unknown .status-dot {
background: #9e9e9e;
}
/* Responsive design */
@media (min-width: 768px) {
.nav-text {
display: inline;
}
.header-nav {
gap: 16px;
}
.nav-item {
padding: 10px 16px;
}
}
@media (max-width: 767px) {
.header-content {
padding: 0 16px;
}
.logo-text {
display: none;
}
.header-nav {
gap: 4px;
}
.nav-item {
padding: 6px 8px;
min-width: 44px;
justify-content: center;
}
}
</style>

42
test-apps/android-test/src/main.ts

@ -1,42 +0,0 @@
/**
* Main Application Entry Point
*
* Vue 3 + TypeScript + Capacitor + vue-facing-decorator setup
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { Capacitor } from '@capacitor/core'
import App from './App.vue'
import router from './router'
// Create Vue app instance
const app = createApp(App)
// Configure Pinia for state management
const pinia = createPinia()
app.use(pinia)
// Configure Vue Router
app.use(router)
// Global error handler for Capacitor
app.config.errorHandler = (err, _instance, info) => {
console.error('Vue Error:', err, info)
if (Capacitor.isNativePlatform()) {
// Log to native platform
console.error('Native Platform Error:', err)
}
}
// Mount the app
app.mount('#app')
// Log platform information
console.log('🚀 Daily Notification Test App Started')
console.log('📱 Platform:', Capacitor.getPlatform())
console.log('🔧 Native Platform:', Capacitor.isNativePlatform())
console.log('🌐 Web Platform:', Capacitor.isPluginAvailable('App'))

109
test-apps/android-test/src/router/index.ts

@ -1,109 +0,0 @@
/**
* Vue Router Configuration
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue'),
meta: {
title: 'Daily Notification Test',
requiresAuth: false
}
},
{
path: '/notifications',
name: 'Notifications',
component: () => import('@/views/NotificationsView.vue'),
meta: {
title: 'Notification Management',
requiresAuth: false
}
},
{
path: '/schedule',
name: 'Schedule',
component: () => import('@/views/ScheduleView.vue'),
meta: {
title: 'Schedule Notification',
requiresAuth: false
}
},
{
path: '/status',
name: 'Status',
component: () => import('@/views/StatusView.vue'),
meta: {
title: 'System Status',
requiresAuth: false
}
},
{
path: '/history',
name: 'History',
component: () => import('@/views/HistoryView.vue'),
meta: {
title: 'Notification History',
requiresAuth: false
}
},
{
path: '/logs',
name: 'Logs',
component: () => import('@/views/LogsView.vue'),
meta: {
title: 'Android Logs',
requiresAuth: false
}
},
{
path: '/settings',
name: 'Settings',
component: () => import('@/views/SettingsView.vue'),
meta: {
title: 'Settings',
requiresAuth: false
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFoundView.vue'),
meta: {
title: 'Page Not Found',
requiresAuth: false
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Global navigation guards
router.beforeEach((to, from, next) => {
// Set page title
if (to.meta?.title) {
document.title = `${to.meta.title} - Daily Notification Test`
}
// Add loading state
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
next()
})
router.afterEach((to) => {
// Clear any previous errors on successful navigation
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`)
})
export default router

299
test-apps/android-test/src/stores/notifications.ts

@ -1,299 +0,0 @@
/**
* Notifications Store - Notification Management State
*
* Pinia store for managing notification scheduling, status, and history
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { Capacitor } from '@capacitor/core'
export interface ScheduledNotification {
id: string
title: string
body: string
scheduledTime: number
deliveredAt?: number
status: 'scheduled' | 'delivered' | 'cancelled'
}
export interface NotificationHistory {
id: string
title: string
body: string
scheduledTime: number
deliveredAt: number
clicked: boolean
dismissed: boolean
}
export const useNotificationsStore = defineStore('notifications', () => {
// State
const scheduledNotifications = ref<ScheduledNotification[]>([])
const notificationHistory = ref<NotificationHistory[]>([])
const isScheduling = ref(false)
const lastError = ref<string | null>(null)
// Getters
const hasScheduledNotifications = computed(() =>
scheduledNotifications.value.length > 0
)
const nextNotification = computed(() => {
const future = scheduledNotifications.value
.filter(n => n.status === 'scheduled' && n.scheduledTime > Date.now())
.sort((a, b) => a.scheduledTime - b.scheduledTime)
return future.length > 0 ? future[0] : null
})
const notificationCount = computed(() =>
scheduledNotifications.value.length
)
// Actions
async function scheduleNotification(options: {
time: string
title?: string
body?: string
sound?: boolean
priority?: string
url?: string
}): Promise<void> {
if (!Capacitor.isNativePlatform()) {
throw new Error('DailyNotification plugin only available on native platforms')
}
if (!window.DailyNotification) {
throw new Error('DailyNotification plugin not loaded. Please restart the app.')
}
try {
isScheduling.value = true
lastError.value = null
await window.DailyNotification.scheduleDailyNotification(options)
// Add to local state (we'll get the actual ID from the plugin)
const notification: ScheduledNotification = {
id: `temp-${Date.now()}`,
title: options.title || 'Daily Update',
body: options.body || 'Your daily notification is ready',
scheduledTime: parseTimeToTimestamp(options.time),
status: 'scheduled'
}
scheduledNotifications.value.push(notification)
console.log('✅ Notification scheduled successfully')
} catch (error) {
const errorMessage = (error as Error).message
lastError.value = errorMessage
console.error('❌ Failed to schedule notification:', errorMessage)
throw error
} finally {
isScheduling.value = false
}
}
async function scheduleReminder(options: {
id: string
title: string
body: string
time: string
sound?: boolean
vibration?: boolean
priority?: string
repeatDaily?: boolean
timezone?: string
}): Promise<void> {
if (!Capacitor.isNativePlatform()) {
throw new Error('DailyNotification plugin only available on native platforms')
}
if (!window.DailyNotification) {
throw new Error('DailyNotification plugin not loaded. Please restart the app.')
}
try {
isScheduling.value = true
lastError.value = null
await window.DailyNotification.scheduleDailyReminder(options)
// Add to local state
const notification: ScheduledNotification = {
id: options.id,
title: options.title,
body: options.body,
scheduledTime: parseTimeToTimestamp(options.time),
status: 'scheduled'
}
scheduledNotifications.value.push(notification)
console.log('✅ Reminder scheduled successfully')
} catch (error) {
const errorMessage = (error as Error).message
lastError.value = errorMessage
console.error('❌ Failed to schedule reminder:', errorMessage)
throw error
} finally {
isScheduling.value = false
}
}
async function cancelReminder(reminderId: string): Promise<void> {
if (!Capacitor.isNativePlatform()) {
throw new Error('DailyNotification plugin only available on native platforms')
}
if (!window.DailyNotification) {
throw new Error('DailyNotification plugin not loaded. Please restart the app.')
}
try {
isScheduling.value = true
lastError.value = null
await window.DailyNotification.cancelDailyReminder({ reminderId })
// Update local state
const index = scheduledNotifications.value.findIndex(n => n.id === reminderId)
if (index !== -1) {
scheduledNotifications.value[index].status = 'cancelled'
}
console.log('✅ Reminder cancelled successfully')
} catch (error) {
const errorMessage = (error as Error).message
lastError.value = errorMessage
console.error('❌ Failed to cancel reminder:', errorMessage)
throw error
} finally {
isScheduling.value = false
}
}
async function checkStatus(): Promise<void> {
if (!Capacitor.isNativePlatform()) {
console.warn('DailyNotification plugin only available on native platforms')
return
}
if (!window.DailyNotification) {
console.warn('DailyNotification plugin not loaded')
return
}
try {
const status = await window.DailyNotification.checkStatus()
console.log('📊 Notification Status:', status)
// Update app store with status
const { useAppStore } = await import('@/stores/app')
const appStore = useAppStore()
appStore.setNotificationStatus(status)
} catch (error) {
console.error('❌ Failed to check notification status:', error)
}
}
async function getLastNotification(): Promise<void> {
if (!Capacitor.isNativePlatform()) {
console.warn('DailyNotification plugin only available on native platforms')
return
}
if (!window.DailyNotification) {
console.warn('DailyNotification plugin not loaded')
return
}
try {
const lastNotification = await window.DailyNotification.getLastNotification()
console.log('📱 Last Notification:', lastNotification)
// Add to history
const historyItem: NotificationHistory = {
id: lastNotification.id,
title: lastNotification.title,
body: lastNotification.body,
scheduledTime: lastNotification.scheduledTime,
deliveredAt: lastNotification.deliveredAt,
clicked: false,
dismissed: false
}
notificationHistory.value.unshift(historyItem)
// Keep only last 50 items
if (notificationHistory.value.length > 50) {
notificationHistory.value = notificationHistory.value.slice(0, 50)
}
} catch (error) {
console.error('❌ Failed to get last notification:', error)
}
}
function clearError(): void {
lastError.value = null
}
function clearHistory(): void {
notificationHistory.value = []
}
// Helper function to parse time string to timestamp
function parseTimeToTimestamp(timeString: string): number {
const [hours, minutes] = timeString.split(':').map(Number)
const now = new Date()
const scheduled = new Date()
scheduled.setHours(hours, minutes, 0, 0)
// If time has passed today, schedule for tomorrow
if (scheduled <= now) {
scheduled.setDate(scheduled.getDate() + 1)
}
return scheduled.getTime()
}
// Reset store
function $reset(): void {
scheduledNotifications.value = []
notificationHistory.value = []
isScheduling.value = false
lastError.value = null
}
return {
// State
scheduledNotifications,
notificationHistory,
isScheduling,
lastError,
// Getters
hasScheduledNotifications,
nextNotification,
notificationCount,
// Actions
scheduleNotification,
scheduleReminder,
cancelReminder,
checkStatus,
getLastNotification,
clearError,
clearHistory,
$reset
}
})

144
test-apps/android-test/src/views/HistoryView.vue

@ -1,144 +0,0 @@
<!--
/**
* History View - Notification History
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="history-view">
<div class="view-header">
<h1 class="page-title">📋 Notification History</h1>
<p class="page-subtitle">View delivered notifications</p>
</div>
<div v-if="hasHistory" class="history-list">
<HistoryItem
v-for="item in notificationHistory"
:key="item.id"
:history="item"
/>
</div>
<div v-else class="no-history">
<div class="empty-state">
<span class="empty-icon">📋</span>
<h3 class="empty-title">No History Available</h3>
<p class="empty-description">
Notification history will appear here after notifications are delivered
</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import HistoryItem from '@/components/items/HistoryItem.vue'
import { useNotificationsStore } from '@/stores/notifications'
@Component({
components: {
HistoryItem
}
})
export default class HistoryView extends Vue {
private notificationsStore = useNotificationsStore()
get hasHistory(): boolean {
return this.notificationHistory.length > 0
}
get notificationHistory() {
return this.notificationsStore.notificationHistory
}
}
</script>
<style scoped>
.history-view {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.view-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.page-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 300;
}
.history-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.no-history {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.empty-icon {
font-size: 4rem;
display: block;
margin-bottom: 16px;
}
.empty-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.empty-description {
font-size: 1rem;
color: rgba(255, 255, 255, 0.8);
margin: 0;
line-height: 1.5;
}
/* Responsive design */
@media (max-width: 768px) {
.history-view {
padding: 16px;
}
.empty-state {
padding: 32px 16px;
}
.empty-icon {
font-size: 3rem;
}
}
</style>

260
test-apps/android-test/src/views/HomeView.vue

@ -1,260 +0,0 @@
<!--
/**
* Home View - Main Dashboard
*
* Vue 3 + vue-facing-decorator + TypeScript
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="home-view">
<!-- Welcome Section -->
<div class="welcome-section">
<h1 class="welcome-title">
🔔 Daily Notification Test
</h1>
<p class="welcome-subtitle">
Vue 3 + Vite + Capacitor + vue-facing-decorator
</p>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<h2 class="section-title">Quick Actions</h2>
<div class="action-grid">
<ActionCard
icon="📅"
title="Schedule Notification"
description="Schedule a new daily notification"
@click="navigateToSchedule"
:loading="isScheduling"
/>
<ActionCard
icon="📊"
title="Check Status"
description="View notification system status"
@click="checkSystemStatus"
:loading="isCheckingStatus"
/>
<ActionCard
icon="📱"
title="View Notifications"
description="Manage scheduled notifications"
@click="navigateToNotifications"
/>
<ActionCard
icon="📋"
title="View History"
description="See notification delivery history"
@click="navigateToHistory"
/>
<ActionCard
icon="📜"
title="View Logs"
description="View and copy Android logs"
@click="navigateToLogs"
/>
</div>
</div>
<!-- System Status -->
<div class="status-section">
<h2 class="section-title">System Status</h2>
<StatusCard :status="notificationStatus" />
</div>
<!-- Next Notification -->
<div v-if="nextNotification" class="next-notification">
<h2 class="section-title">Next Notification</h2>
<NotificationCard :notification="nextNotification" />
</div>
<!-- Recent Activity -->
<div v-if="recentHistory.length > 0" class="recent-activity">
<h2 class="section-title">Recent Activity</h2>
<div class="activity-list">
<ActivityItem
v-for="item in recentHistory"
:key="item.id"
:activity="item"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { Capacitor } from '@capacitor/core'
import ActionCard from '@/components/cards/ActionCard.vue'
import StatusCard from '@/components/cards/StatusCard.vue'
import NotificationCard from '@/components/cards/NotificationCard.vue'
import ActivityItem from '@/components/items/ActivityItem.vue'
import { useAppStore } from '@/stores/app'
import { useNotificationsStore } from '@/stores/notifications'
@Component({
components: {
ActionCard,
StatusCard,
NotificationCard,
ActivityItem
}
})
export default class HomeView extends Vue {
private appStore = useAppStore()
private notificationsStore = useNotificationsStore()
isCheckingStatus = false
get isScheduling(): boolean {
return this.notificationsStore.isScheduling
}
get notificationStatus() {
return this.appStore.notificationStatus
}
get nextNotification() {
return this.notificationsStore.nextNotification
}
get recentHistory() {
return this.notificationsStore.notificationHistory.slice(0, 5)
}
async mounted(): Promise<void> {
await this.initializeHome()
}
private async initializeHome(): Promise<void> {
try {
// Check system status
await this.checkSystemStatus()
// Load recent notifications
await this.notificationsStore.getLastNotification()
} catch (error) {
console.error('❌ Failed to initialize home:', error)
this.appStore.setError('Failed to load home data: ' + (error as Error).message)
}
}
async checkSystemStatus(): Promise<void> {
if (!Capacitor.isNativePlatform()) {
return
}
try {
this.isCheckingStatus = true
await this.notificationsStore.checkStatus()
} catch (error) {
console.error('❌ Failed to check system status:', error)
} finally {
this.isCheckingStatus = false
}
}
navigateToSchedule(): void {
this.$router.push('/schedule')
}
navigateToNotifications(): void {
this.$router.push('/notifications')
}
navigateToHistory(): void {
this.$router.push('/history')
}
navigateToLogs(): void {
this.$router.push('/logs')
}
}
</script>
<style scoped>
.home-view {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.welcome-section {
text-align: center;
margin-bottom: 32px;
padding: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
}
.welcome-title {
font-size: 2.5rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.welcome-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 300;
}
.section-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0 0 16px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.quick-actions {
margin-bottom: 32px;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.status-section {
margin-bottom: 32px;
}
.next-notification {
margin-bottom: 32px;
}
.recent-activity {
margin-bottom: 32px;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Responsive design */
@media (max-width: 768px) {
.home-view {
padding: 16px;
}
.welcome-title {
font-size: 2rem;
}
.action-grid {
grid-template-columns: 1fr;
}
}
</style>

629
test-apps/android-test/src/views/LogsView.vue

@ -1,629 +0,0 @@
<!--
/**
* Logs View - View and Copy Android Logs
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="logs-view">
<div class="view-header">
<h1 class="page-title">📋 Android Logs</h1>
<p class="page-subtitle">View and copy DailyNotification plugin logs</p>
</div>
<div class="logs-controls">
<button
class="control-button refresh-button"
@click="refreshLogs"
:disabled="isRefreshing"
>
<span v-if="isRefreshing">🔄</span>
<span v-else>🔄</span>
{{ isRefreshing ? 'Refreshing...' : 'Refresh Logs' }}
</button>
<button
class="control-button copy-button"
@click="copyLogsToClipboard"
:disabled="!hasLogs || isCopying"
>
<span v-if="isCopying">📋</span>
<span v-else>📋</span>
{{ isCopying ? 'Copying...' : 'Copy to Clipboard' }}
</button>
<button
class="control-button clear-button"
@click="clearLogs"
:disabled="!hasLogs"
>
🗑 Clear Logs
</button>
</div>
<div class="logs-container">
<div v-if="hasLogs" class="logs-content">
<div class="logs-header">
<span class="logs-count">{{ logs.length }} log entries</span>
<span class="logs-timestamp">Last updated: {{ lastUpdated }}</span>
</div>
<div class="logs-list" ref="logsList">
<div
v-for="(log, index) in logs"
:key="index"
class="log-entry"
:class="getLogLevelClass(log)"
>
<span class="log-timestamp">{{ formatTimestamp(log.timestamp) }}</span>
<span class="log-level">{{ log.level }}</span>
<span class="log-tag">{{ log.tag }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
<div v-else class="no-logs">
<div class="empty-state">
<span class="empty-icon">📋</span>
<h3 class="empty-title">No Logs Available</h3>
<p class="empty-description">
Click "Refresh Logs" to fetch DailyNotification plugin logs from the device
</p>
</div>
</div>
</div>
<!-- Success/Error Messages -->
<div v-if="successMessage" class="message success-message">
{{ successMessage }}
</div>
<div v-if="errorMessage" class="message error-message">
{{ errorMessage }}
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { Capacitor } from '@capacitor/core'
interface LogEntry {
timestamp: string
level: string
tag: string
message: string
}
@Component
export default class LogsView extends Vue {
logs: LogEntry[] = []
isRefreshing = false
isCopying = false
successMessage = ''
errorMessage = ''
lastUpdated = ''
get hasLogs(): boolean {
return this.logs.length > 0
}
async refreshLogs(): Promise<void> {
this.isRefreshing = true
this.clearMessages()
try {
if (!Capacitor.isNativePlatform()) {
// Mock logs for web testing
this.logs = this.generateMockLogs()
this.lastUpdated = new Date().toLocaleString()
this.successMessage = 'Mock logs loaded (web mode)'
return
}
// In a real implementation, you would fetch logs from the device
// For now, we'll simulate fetching logs
await this.simulateLogFetch()
} catch (error) {
console.error('❌ Failed to refresh logs:', error)
this.errorMessage = 'Failed to refresh logs: ' + (error as Error).message
} finally {
this.isRefreshing = false
}
}
async copyLogsToClipboard(): Promise<void> {
if (!this.hasLogs) {
this.errorMessage = 'No logs to copy'
return
}
this.isCopying = true
this.clearMessages()
try {
const logsText = this.formatLogsForCopy()
if (Capacitor.isNativePlatform()) {
// Use Capacitor Clipboard plugin if available
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Clipboard) {
await window.Capacitor.Plugins.Clipboard.write({
string: logsText
})
} else {
// Fallback to web clipboard API
await navigator.clipboard.writeText(logsText)
}
} else {
// Web clipboard API
await navigator.clipboard.writeText(logsText)
}
this.successMessage = `Copied ${this.logs.length} log entries to clipboard`
// Auto-hide success message
setTimeout(() => {
this.successMessage = ''
}, 3000)
} catch (error) {
console.error('❌ Failed to copy logs:', error)
this.errorMessage = 'Failed to copy logs: ' + (error as Error).message
} finally {
this.isCopying = false
}
}
clearLogs(): void {
this.logs = []
this.lastUpdated = ''
this.clearMessages()
this.successMessage = 'Logs cleared'
// Auto-hide success message
setTimeout(() => {
this.successMessage = ''
}, 2000)
}
private clearMessages(): void {
this.successMessage = ''
this.errorMessage = ''
}
private async simulateLogFetch(): Promise<void> {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000))
// Generate sample logs based on the terminal output you showed
this.logs = this.generateSampleLogs()
this.lastUpdated = new Date().toLocaleString()
this.successMessage = `Loaded ${this.logs.length} log entries`
// Auto-hide success message
setTimeout(() => {
this.successMessage = ''
}, 3000)
}
private generateSampleLogs(): LogEntry[] {
const now = new Date()
return [
{
timestamp: new Date(now.getTime() - 1000).toISOString(),
level: 'D',
tag: 'DailyNotificationReceiver',
message: 'DN|RECEIVE_START action=com.timesafari.daily.NOTIFICATION'
},
{
timestamp: new Date(now.getTime() - 900).toISOString(),
level: 'D',
tag: 'DailyNotificationReceiver',
message: 'DN|WORK_ENQUEUE display=3bc1b920-9407-4ccf-94c0-26f99ba4c39d'
},
{
timestamp: new Date(now.getTime() - 800).toISOString(),
level: 'D',
tag: 'DailyNotificationWorker',
message: 'DN|WORK_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d action=display'
},
{
timestamp: new Date(now.getTime() - 700).toISOString(),
level: 'D',
tag: 'DailyNotificationWorker',
message: 'DN|DISPLAY_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d'
},
{
timestamp: new Date(now.getTime() - 600).toISOString(),
level: 'D',
tag: 'DailyNotificationStorage',
message: 'Loading notifications from storage: [53 notifications]'
},
{
timestamp: new Date(now.getTime() - 500).toISOString(),
level: 'D',
tag: 'DailyNotificationWorker',
message: 'DN|JIT_FRESH skip=true ageMin=0 id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d'
},
{
timestamp: new Date(now.getTime() - 400).toISOString(),
level: 'D',
tag: 'DailyNotificationWorker',
message: 'DN|CLICK_INTENT app_only'
},
{
timestamp: new Date(now.getTime() - 300).toISOString(),
level: 'D',
tag: 'DailyNotificationWorker',
message: 'DN|ACTION_BUTTONS dismiss_only'
},
{
timestamp: new Date(now.getTime() - 200).toISOString(),
level: 'I',
tag: 'DailyNotificationWorker',
message: 'DN|DISPLAY_NOTIF_OK id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d'
},
{
timestamp: new Date(now.getTime() - 100).toISOString(),
level: 'D',
tag: 'DailyNotificationWorker',
message: 'DN|RESCHEDULE_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d'
}
]
}
private generateMockLogs(): LogEntry[] {
const now = new Date()
return [
{
timestamp: new Date(now.getTime() - 2000).toISOString(),
level: 'I',
tag: 'VueApp',
message: 'App initialized successfully'
},
{
timestamp: new Date(now.getTime() - 1500).toISOString(),
level: 'D',
tag: 'Capacitor',
message: 'Running in web mode - DailyNotification plugin not available'
},
{
timestamp: new Date(now.getTime() - 1000).toISOString(),
level: 'W',
tag: 'NotificationsStore',
message: 'DailyNotification plugin not loaded'
}
]
}
private formatLogsForCopy(): string {
const header = `DailyNotification Plugin Logs\nGenerated: ${new Date().toLocaleString()}\nTotal Entries: ${this.logs.length}\n\n`
const logLines = this.logs.map(log =>
`${log.timestamp} ${log.level}/${log.tag}: ${log.message}`
).join('\n')
return header + logLines
}
private formatTimestamp(timestamp: string): string {
const date = new Date(timestamp)
return date.toLocaleTimeString()
}
private getLogLevelClass(log: LogEntry): string {
switch (log.level) {
case 'E': return 'log-error'
case 'W': return 'log-warning'
case 'I': return 'log-info'
case 'D': return 'log-debug'
case 'V': return 'log-verbose'
default: return 'log-default'
}
}
async mounted(): Promise<void> {
// Auto-refresh logs on mount
await this.refreshLogs()
}
}
</script>
<style scoped>
.logs-view {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.view-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.page-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 300;
}
.logs-controls {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.control-button {
background: linear-gradient(135deg, #2196f3, #1976d2);
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 8px;
}
.control-button:hover:not(:disabled) {
background: linear-gradient(135deg, #1976d2, #1565c0);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(33, 150, 243, 0.3);
}
.control-button:disabled {
background: rgba(255, 255, 255, 0.2);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.refresh-button {
background: linear-gradient(135deg, #4caf50, #45a049);
}
.refresh-button:hover:not(:disabled) {
background: linear-gradient(135deg, #45a049, #3d8b40);
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
}
.copy-button {
background: linear-gradient(135deg, #ff9800, #f57c00);
}
.copy-button:hover:not(:disabled) {
background: linear-gradient(135deg, #f57c00, #ef6c00);
box-shadow: 0 8px 25px rgba(255, 152, 0, 0.3);
}
.clear-button {
background: linear-gradient(135deg, #f44336, #d32f2f);
}
.clear-button:hover:not(:disabled) {
background: linear-gradient(135deg, #d32f2f, #c62828);
box-shadow: 0 8px 25px rgba(244, 67, 54, 0.3);
}
.logs-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
}
.logs-content {
padding: 20px;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.logs-count {
font-weight: 600;
color: white;
font-size: 0.9rem;
}
.logs-timestamp {
color: rgba(255, 255, 255, 0.7);
font-size: 0.8rem;
}
.logs-list {
max-height: 500px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.4;
}
.log-entry {
display: grid;
grid-template-columns: 80px 20px 200px 1fr;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
align-items: start;
}
.log-entry:last-child {
border-bottom: none;
}
.log-timestamp {
color: rgba(255, 255, 255, 0.6);
font-size: 0.75rem;
}
.log-level {
font-weight: bold;
text-align: center;
font-size: 0.8rem;
}
.log-tag {
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-message {
color: rgba(255, 255, 255, 0.9);
word-break: break-word;
}
/* Log level colors */
.log-error .log-level {
color: #f44336;
}
.log-warning .log-level {
color: #ff9800;
}
.log-info .log-level {
color: #4caf50;
}
.log-debug .log-level {
color: #2196f3;
}
.log-verbose .log-level {
color: #9c27b0;
}
.log-default .log-level {
color: rgba(255, 255, 255, 0.7);
}
.no-logs {
padding: 40px 20px;
text-align: center;
}
.empty-state {
padding: 40px 20px;
}
.empty-icon {
font-size: 4rem;
display: block;
margin-bottom: 16px;
}
.empty-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.empty-description {
font-size: 1rem;
color: rgba(255, 255, 255, 0.8);
margin: 0;
line-height: 1.5;
}
.message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
font-weight: 600;
z-index: 1000;
animation: slideIn 0.3s ease;
}
.success-message {
background: linear-gradient(135deg, #4caf50, #45a049);
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.error-message {
background: linear-gradient(135deg, #f44336, #d32f2f);
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive design */
@media (max-width: 768px) {
.logs-view {
padding: 16px;
}
.logs-controls {
flex-direction: column;
}
.control-button {
justify-content: center;
}
.log-entry {
grid-template-columns: 1fr;
gap: 4px;
}
.log-timestamp,
.log-level,
.log-tag {
font-size: 0.75rem;
}
.logs-list {
font-size: 0.8rem;
}
.message {
position: relative;
top: auto;
right: auto;
margin: 16px 0;
}
}
</style>

117
test-apps/android-test/src/views/NotFoundView.vue

@ -1,117 +0,0 @@
<!--
/**
* Not Found View - 404 Page
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="not-found-view">
<div class="not-found-content">
<span class="not-found-icon">🔍</span>
<h1 class="not-found-title">404</h1>
<h2 class="not-found-subtitle">Page Not Found</h2>
<p class="not-found-description">
The page you're looking for doesn't exist.
</p>
<button class="home-button" @click="goHome">
🏠 Go Home
</button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class NotFoundView extends Vue {
goHome(): void {
this.$router.push('/')
}
}
</script>
<style scoped>
.not-found-view {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
padding: 20px;
}
.not-found-content {
text-align: center;
padding: 40px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.not-found-icon {
font-size: 4rem;
display: block;
margin-bottom: 16px;
}
.not-found-title {
font-size: 4rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.not-found-subtitle {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0 0 16px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.not-found-description {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 24px 0;
line-height: 1.5;
}
.home-button {
background: linear-gradient(135deg, #4caf50, #45a049);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.home-button:hover {
background: linear-gradient(135deg, #45a049, #3d8b40);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
}
/* Responsive design */
@media (max-width: 768px) {
.not-found-content {
padding: 32px 20px;
}
.not-found-title {
font-size: 3rem;
}
.not-found-icon {
font-size: 3rem;
}
}
</style>

179
test-apps/android-test/src/views/NotificationsView.vue

@ -1,179 +0,0 @@
<!--
/**
* Notifications View - Manage Scheduled Notifications
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="notifications-view">
<div class="view-header">
<h1 class="page-title">📱 Scheduled Notifications</h1>
<p class="page-subtitle">Manage your daily notifications</p>
</div>
<div v-if="hasScheduledNotifications" class="notifications-list">
<NotificationCard
v-for="notification in scheduledNotifications"
:key="notification.id"
:notification="notification"
@cancel="handleCancelNotification"
/>
</div>
<div v-else class="no-notifications">
<div class="empty-state">
<span class="empty-icon">📅</span>
<h3 class="empty-title">No Notifications Scheduled</h3>
<p class="empty-description">
Schedule your first daily notification to get started
</p>
<button class="empty-action" @click="navigateToSchedule">
📅 Schedule Notification
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import NotificationCard from '@/components/cards/NotificationCard.vue'
import { useNotificationsStore } from '@/stores/notifications'
@Component({
components: {
NotificationCard
}
})
export default class NotificationsView extends Vue {
private notificationsStore = useNotificationsStore()
get hasScheduledNotifications(): boolean {
return this.notificationsStore.hasScheduledNotifications
}
get scheduledNotifications() {
return this.notificationsStore.scheduledNotifications
}
async handleCancelNotification(notificationId: string): Promise<void> {
try {
await this.notificationsStore.cancelReminder(notificationId)
} catch (error) {
console.error('❌ Failed to cancel notification:', error)
}
}
navigateToSchedule(): void {
this.$router.push('/schedule')
}
}
</script>
<style scoped>
.notifications-view {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.view-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.page-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 300;
}
.notifications-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.no-notifications {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.empty-icon {
font-size: 4rem;
display: block;
margin-bottom: 16px;
}
.empty-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.empty-description {
font-size: 1rem;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 24px 0;
line-height: 1.5;
}
.empty-action {
background: linear-gradient(135deg, #4caf50, #45a049);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.empty-action:hover {
background: linear-gradient(135deg, #45a049, #3d8b40);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
}
/* Responsive design */
@media (max-width: 768px) {
.notifications-view {
padding: 16px;
}
.empty-state {
padding: 32px 16px;
}
.empty-icon {
font-size: 3rem;
}
}
</style>

518
test-apps/android-test/src/views/ScheduleView.vue

@ -1,518 +0,0 @@
<!--
/**
* Schedule View - Schedule New Notifications
*
* Vue 3 + vue-facing-decorator + TypeScript
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="schedule-view">
<div class="schedule-header">
<h1 class="page-title">📅 Schedule Notification</h1>
<p class="page-subtitle">Create a new daily notification</p>
</div>
<div class="schedule-form">
<form @submit.prevent="handleSubmit">
<!-- Time Input -->
<div class="form-group">
<label class="form-label">Time</label>
<input
v-model="form.time"
type="time"
class="form-input"
required
:disabled="isScheduling"
/>
</div>
<!-- Title Input -->
<div class="form-group">
<label class="form-label">Title</label>
<input
v-model="form.title"
type="text"
class="form-input"
placeholder="Daily Update"
:disabled="isScheduling"
/>
</div>
<!-- Body Input -->
<div class="form-group">
<label class="form-label">Message</label>
<textarea
v-model="form.body"
class="form-textarea"
placeholder="Your daily notification message"
rows="3"
:disabled="isScheduling"
></textarea>
</div>
<!-- Options -->
<div class="form-options">
<div class="option-group">
<label class="option-label">
<input
v-model="form.sound"
type="checkbox"
class="option-checkbox"
:disabled="isScheduling"
/>
<span class="option-text">🔊 Sound</span>
</label>
</div>
<div class="option-group">
<label class="option-label">
<span class="option-text">Priority</span>
<select
v-model="form.priority"
class="form-select"
:disabled="isScheduling"
>
<option value="low">Low</option>
<option value="default">Default</option>
<option value="high">High</option>
</select>
</label>
</div>
</div>
<!-- URL Input -->
<div class="form-group">
<label class="form-label">URL (Optional)</label>
<input
v-model="form.url"
type="url"
class="form-input"
placeholder="https://example.com"
:disabled="isScheduling"
/>
</div>
<!-- Submit Button -->
<div class="form-actions">
<button
type="submit"
class="submit-button"
:disabled="isScheduling || !isFormValid"
:class="{ 'loading': isScheduling }"
>
<span v-if="isScheduling" class="button-spinner"></span>
<span v-else>📅 Schedule Notification</span>
</button>
</div>
</form>
</div>
<!-- Quick Schedule Options -->
<div class="quick-schedule">
<h2 class="section-title">Quick Schedule</h2>
<div class="quick-options">
<button
v-for="option in quickOptions"
:key="option.label"
class="quick-button"
@click="fillQuickOption(option)"
:disabled="isScheduling"
>
<span class="quick-icon">{{ option.icon }}</span>
<span class="quick-label">{{ option.label }}</span>
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { useAppStore } from '@/stores/app'
import { useNotificationsStore } from '@/stores/notifications'
interface QuickOption {
label: string
icon: string
time: string
title: string
body: string
}
interface ScheduleForm {
time: string
title: string
body: string
sound: boolean
priority: string
url: string
}
@Component
export default class ScheduleView extends Vue {
private appStore = useAppStore()
private notificationsStore = useNotificationsStore()
form: ScheduleForm = {
time: '',
title: 'Daily Update',
body: 'Your daily notification is ready',
sound: true,
priority: 'default',
url: ''
}
quickOptions: QuickOption[] = [
{
label: 'Morning',
icon: '🌅',
time: '08:00',
title: 'Good Morning!',
body: 'Ready to make today amazing?'
},
{
label: 'Lunch',
icon: '🍽️',
time: '12:00',
title: 'Lunch Break',
body: 'Time for a well-deserved break!'
},
{
label: 'Evening',
icon: '🌆',
time: '18:00',
title: 'Evening Update',
body: 'How was your day?'
},
{
label: 'Bedtime',
icon: '🌙',
time: '22:00',
title: 'Bedtime Reminder',
body: 'Time to wind down and rest'
}
]
get isScheduling(): boolean {
return this.notificationsStore.isScheduling
}
get isFormValid(): boolean {
return this.form.time.length > 0 &&
this.form.title.trim().length > 0 &&
this.form.body.trim().length > 0
}
mounted(): void {
this.initializeForm()
}
private initializeForm(): void {
// Set default time to 1 hour from now
const now = new Date()
now.setHours(now.getHours() + 1)
this.form.time = now.toTimeString().slice(0, 5)
}
async handleSubmit(): Promise<void> {
if (!this.isFormValid || this.isScheduling) {
return
}
try {
await this.notificationsStore.scheduleNotification({
time: this.form.time,
title: this.form.title.trim(),
body: this.form.body.trim(),
sound: this.form.sound,
priority: this.form.priority,
url: this.form.url.trim() || undefined
})
// Show success message
this.appStore.setError('Notification scheduled successfully!')
// Reset form
this.resetForm()
// Navigate back to home
setTimeout(() => {
this.$router.push('/')
}, 1500)
} catch (error) {
console.error('❌ Failed to schedule notification:', error)
this.appStore.setError('Failed to schedule notification: ' + (error as Error).message)
}
}
fillQuickOption(option: QuickOption): void {
this.form.time = option.time
this.form.title = option.title
this.form.body = option.body
}
resetForm(): void {
this.form = {
time: '',
title: 'Daily Update',
body: 'Your daily notification is ready',
sound: true,
priority: 'default',
url: ''
}
this.initializeForm()
}
}
</script>
<style scoped>
.schedule-view {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.schedule-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.page-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 300;
}
.schedule-form {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 24px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
margin-bottom: 32px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 0.9rem;
font-weight: 600;
color: white;
margin-bottom: 8px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.15);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.option-group {
display: flex;
align-items: center;
}
.option-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: white;
font-size: 0.9rem;
font-weight: 500;
}
.option-checkbox {
width: 18px;
height: 18px;
accent-color: #4caf50;
}
.option-text {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.form-select {
margin-left: auto;
width: auto;
min-width: 100px;
}
.form-actions {
margin-top: 24px;
}
.submit-button {
width: 100%;
padding: 16px 24px;
background: linear-gradient(135deg, #4caf50, #45a049);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.submit-button:hover:not(:disabled) {
background: linear-gradient(135deg, #45a049, #3d8b40);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
}
.submit-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.submit-button.loading {
background: linear-gradient(135deg, #9e9e9e, #757575);
}
.button-spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.quick-schedule {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 24px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.section-title {
font-size: 1.3rem;
font-weight: 600;
color: white;
margin: 0 0 16px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.quick-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.quick-button {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 12px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
}
.quick-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.quick-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.quick-icon {
font-size: 1.5rem;
}
.quick-label {
font-size: 0.85rem;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.schedule-view {
padding: 16px;
}
.schedule-form {
padding: 20px;
}
.form-options {
grid-template-columns: 1fr;
}
.quick-options {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

310
test-apps/android-test/src/views/StatusView.vue

@ -1,310 +0,0 @@
<!--
/**
* Status View - System Status and Diagnostics
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="status-view">
<div class="view-header">
<h1 class="page-title">📊 System Status</h1>
<p class="page-subtitle">Notification system diagnostics</p>
</div>
<!-- Status Overview -->
<div class="status-overview">
<StatusCard
:status="notificationStatus"
@refresh="refreshStatus"
/>
</div>
<!-- Action Buttons -->
<div class="status-actions">
<h2 class="section-title">Actions</h2>
<div class="action-grid">
<button
class="action-button"
@click="refreshStatus"
:disabled="isRefreshing"
>
<span class="action-icon">🔄</span>
<span class="action-text">Refresh Status</span>
</button>
<button
class="action-button"
@click="openChannelSettings"
>
<span class="action-icon">🔧</span>
<span class="action-text">Channel Settings</span>
</button>
<button
class="action-button"
@click="openExactAlarmSettings"
>
<span class="action-icon"></span>
<span class="action-text">Alarm Settings</span>
</button>
<button
class="action-button"
@click="testNotification"
:disabled="!canScheduleNow"
>
<span class="action-icon">🧪</span>
<span class="action-text">Test Notification</span>
</button>
</div>
</div>
<!-- Detailed Information -->
<div v-if="notificationStatus" class="detailed-info">
<h2 class="section-title">Detailed Information</h2>
<div class="info-grid">
<InfoCard
title="Platform"
:value="platformInfo"
icon="📱"
/>
<InfoCard
title="Android Version"
:value="`API ${notificationStatus.androidVersion}`"
icon="🤖"
/>
<InfoCard
title="Channel ID"
:value="notificationStatus.channelId"
icon="📢"
/>
<InfoCard
title="Next Scheduled"
:value="nextScheduledText"
icon="⏰"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { Capacitor } from '@capacitor/core'
import StatusCard from '@/components/cards/StatusCard.vue'
import InfoCard from '@/components/cards/InfoCard.vue'
import { useAppStore } from '@/stores/app'
import { useNotificationsStore } from '@/stores/notifications'
@Component({
components: {
StatusCard,
InfoCard
}
})
export default class StatusView extends Vue {
private appStore = useAppStore()
private notificationsStore = useNotificationsStore()
isRefreshing = false
get notificationStatus() {
return this.appStore.notificationStatus
}
get canScheduleNow(): boolean {
return this.notificationStatus?.canScheduleNow ?? false
}
get platformInfo(): string {
const platform = Capacitor.getPlatform()
const isNative = Capacitor.isNativePlatform()
return `${platform} ${isNative ? '(Native)' : '(Web)'}`
}
get nextScheduledText(): string {
if (!this.notificationStatus || this.notificationStatus.nextScheduledAt <= 0) {
return 'None'
}
const date = new Date(this.notificationStatus.nextScheduledAt)
return date.toLocaleString()
}
async mounted(): Promise<void> {
await this.refreshStatus()
}
async refreshStatus(): Promise<void> {
try {
this.isRefreshing = true
await this.notificationsStore.checkStatus()
} catch (error) {
console.error('❌ Failed to refresh status:', error)
} finally {
this.isRefreshing = false
}
}
async openChannelSettings(): Promise<void> {
if (!Capacitor.isNativePlatform() || !window.DailyNotification) {
return
}
try {
await window.DailyNotification.openChannelSettings()
} catch (error) {
console.error('❌ Failed to open channel settings:', error)
}
}
async openExactAlarmSettings(): Promise<void> {
if (!Capacitor.isNativePlatform() || !window.DailyNotification) {
return
}
try {
await window.DailyNotification.openExactAlarmSettings()
} catch (error) {
console.error('❌ Failed to open exact alarm settings:', error)
}
}
async testNotification(): Promise<void> {
try {
await this.notificationsStore.scheduleNotification({
time: this.getTestTime(),
title: 'Test Notification',
body: 'This is a test notification from the status page',
sound: true,
priority: 'high'
})
this.appStore.setError('Test notification scheduled successfully!')
} catch (error) {
console.error('❌ Failed to schedule test notification:', error)
this.appStore.setError('Failed to schedule test notification: ' + (error as Error).message)
}
}
getTestTime(): string {
const now = new Date()
now.setMinutes(now.getMinutes() + 2) // 2 minutes from now
return now.toTimeString().slice(0, 5)
}
}
</script>
<style scoped>
.status-view {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.view-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.page-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 300;
}
.status-overview {
margin-bottom: 32px;
}
.status-actions {
margin-bottom: 32px;
}
.section-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0 0 16px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.action-button {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
text-decoration: none;
}
.action-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-icon {
font-size: 1.2rem;
}
.action-text {
font-size: 0.95rem;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.detailed-info {
margin-bottom: 32px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
/* Responsive design */
@media (max-width: 768px) {
.status-view {
padding: 16px;
}
.action-grid {
grid-template-columns: 1fr;
}
.info-grid {
grid-template-columns: 1fr;
}
}
</style>

45
test-apps/android-test/tsconfig.json

@ -1,45 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@views/*": ["./src/views/*"],
"@stores/*": ["./src/stores/*"],
"@services/*": ["./src/services/*"],
"@types/*": ["./src/types/*"],
"@utils/*": ["./src/utils/*"]
},
"types": [
"vite/client",
"node"
]
},
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*",
"node_modules",
"dist"
]
}

70
test-apps/android-test/vite.config.ts

@ -1,70 +0,0 @@
/**
* Vite Configuration for Daily Notification Test App
*
* Vue 3 + TypeScript + Capacitor setup with vue-facing-decorator support
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
// Build configuration for Capacitor
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
},
// Development server configuration
server: {
host: '0.0.0.0',
port: 3000,
strictPort: true,
hmr: {
port: 3001
}
},
// Preview server configuration
preview: {
host: '0.0.0.0',
port: 4173,
strictPort: true
},
// Path resolution
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@views': resolve(__dirname, 'src/views'),
'@stores': resolve(__dirname, 'src/stores'),
'@services': resolve(__dirname, 'src/services'),
'@types': resolve(__dirname, 'src/types'),
'@utils': resolve(__dirname, 'src/utils')
}
},
// TypeScript configuration
esbuild: {
target: 'es2020'
},
// Define global constants
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}
})

33
test-apps/android-test/webpack.config.js

@ -1,33 +0,0 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
devServer: {
static: './dist',
port: 3000,
hot: true,
},
};

112
test-apps/check-environment.sh

@ -1,112 +0,0 @@
#!/bin/bash
# Environment Verification Script for Test Apps
echo "🔍 Verifying Test Apps Environment..."
echo ""
# Check Node.js
echo "📦 Node.js:"
if command -v node &> /dev/null; then
node_version=$(node --version)
echo " ✅ Installed: $node_version"
# Check if version is 18+
major_version=$(echo $node_version | cut -d'.' -f1 | sed 's/v//')
if [ "$major_version" -ge 18 ]; then
echo " ✅ Version 18+ (compatible)"
else
echo " ⚠️ Version $major_version (requires 18+)"
fi
else
echo " ❌ Not installed"
fi
# Check npm
echo ""
echo "📦 npm:"
if command -v npm &> /dev/null; then
npm_version=$(npm --version)
echo " ✅ Installed: $npm_version"
else
echo " ❌ Not installed"
fi
# Check Capacitor CLI
echo ""
echo "⚡ Capacitor CLI:"
if command -v cap &> /dev/null; then
cap_version=$(cap --version)
echo " ✅ Installed: $cap_version"
else
echo " ❌ Not installed (will be installed by setup scripts)"
fi
# Check Android (if available)
echo ""
echo "📱 Android:"
if command -v studio &> /dev/null; then
echo " ✅ Android Studio installed"
else
echo " ❌ Android Studio not found"
fi
if [ ! -z "$ANDROID_HOME" ]; then
echo " ✅ ANDROID_HOME set: $ANDROID_HOME"
else
echo " ❌ ANDROID_HOME not set"
fi
if command -v java &> /dev/null; then
java_version=$(java -version 2>&1 | head -n 1)
echo " ✅ Java: $java_version"
else
echo " ❌ Java not found"
fi
# Check iOS (if on macOS)
echo ""
echo "🍎 iOS:"
if [[ "$OSTYPE" == "darwin"* ]]; then
if command -v xcodebuild &> /dev/null; then
xcode_version=$(xcodebuild -version | head -n 1)
echo " ✅ Xcode: $xcode_version"
else
echo " ❌ Xcode not installed"
fi
if command -v xcrun &> /dev/null; then
echo " ✅ Xcode Command Line Tools available"
else
echo " ❌ Xcode Command Line Tools not installed"
fi
else
echo " ⚠️ iOS development requires macOS"
fi
# Check Electron
echo ""
echo "⚡ Electron:"
if command -v npx &> /dev/null; then
electron_version=$(npx electron --version 2>/dev/null)
if [ $? -eq 0 ]; then
echo " ✅ Electron available: $electron_version"
else
echo " ⚠️ Electron not installed (will be installed by setup)"
fi
else
echo " ❌ npx not available"
fi
echo ""
echo "📋 Summary:"
echo " - Node.js 18+: $(command -v node &> /dev/null && node --version | cut -d'.' -f1 | sed 's/v//' | awk '{if($1>=18) print "✅"; else print "❌"}' || echo "❌")"
echo " - npm: $(command -v npm &> /dev/null && echo "✅" || echo "❌")"
echo " - Android Studio: $(command -v studio &> /dev/null && echo "✅" || echo "❌")"
echo " - Xcode: $(command -v xcodebuild &> /dev/null && echo "✅" || echo "❌")"
echo " - Electron: $(command -v npx &> /dev/null && npx electron --version &> /dev/null && echo "✅" || echo "❌")"
echo ""
echo "🚀 Next Steps:"
echo " 1. Install missing prerequisites"
echo " 2. Run setup scripts: ./setup-*.sh"
echo " 3. See SETUP_GUIDE.md for detailed instructions"

152
test-apps/config/timesafari-config.json

@ -1,152 +0,0 @@
{
"timesafari": {
"appId": "app.timesafari.test",
"appName": "TimeSafari Test",
"version": "1.0.0",
"description": "Test app for TimeSafari Daily Notification Plugin integration"
},
"endorser": {
"baseUrl": "http://localhost:3001",
"apiVersion": "v2",
"endpoints": {
"offers": "/api/v2/report/offers",
"offersToPlans": "/api/v2/report/offersToPlansOwnedByMe",
"plansLastUpdated": "/api/v2/report/plansLastUpdatedBetween",
"notificationsBundle": "/api/v2/report/notifications/bundle"
},
"authentication": {
"type": "Bearer",
"token": "test-jwt-token-12345",
"headers": {
"Authorization": "Bearer test-jwt-token-12345",
"Content-Type": "application/json",
"X-Privacy-Level": "user-controlled"
}
},
"pagination": {
"defaultLimit": 50,
"maxLimit": 100,
"hitLimitThreshold": 50
}
},
"notificationTypes": {
"offers": {
"enabled": true,
"types": [
"new_to_me",
"changed_to_me",
"new_to_projects",
"changed_to_projects",
"new_to_favorites",
"changed_to_favorites"
]
},
"projects": {
"enabled": true,
"types": [
"local_new",
"local_changed",
"content_interest_new",
"favorited_changed"
]
},
"people": {
"enabled": true,
"types": [
"local_new",
"local_changed",
"content_interest_new",
"favorited_changed",
"contacts_changed"
]
},
"items": {
"enabled": true,
"types": [
"local_new",
"local_changed",
"favorited_changed"
]
}
},
"scheduling": {
"contentFetch": {
"schedule": "0 8 * * *",
"time": "08:00",
"description": "8 AM daily - fetch community updates"
},
"userNotification": {
"schedule": "0 9 * * *",
"time": "09:00",
"description": "9 AM daily - notify users of community updates"
}
},
"testData": {
"userDid": "did:example:testuser123",
"starredPlanIds": [
"plan-community-garden",
"plan-local-food",
"plan-sustainability"
],
"lastKnownOfferId": "01HSE3R9MAC0FT3P3KZ382TWV7",
"lastKnownPlanId": "01HSE3R9MAC0FT3P3KZ382TWV8",
"mockOffers": [
{
"jwtId": "01HSE3R9MAC0FT3P3KZ382TWV7",
"handleId": "offer-web-dev-001",
"offeredByDid": "did:example:offerer123",
"recipientDid": "did:example:testuser123",
"objectDescription": "Web development services for community project",
"unit": "USD",
"amount": 1000,
"amountGiven": 500,
"amountGivenConfirmed": 250
}
],
"mockProjects": [
{
"plan": {
"jwtId": "01HSE3R9MAC0FT3P3KZ382TWV8",
"handleId": "plan-community-garden",
"name": "Community Garden Project",
"description": "Building a community garden for local food production",
"issuerDid": "did:example:issuer123",
"agentDid": "did:example:agent123"
},
"wrappedClaimBefore": null
}
]
},
"callbacks": {
"offers": {
"enabled": true,
"localHandler": "handleOffersNotification"
},
"projects": {
"enabled": true,
"localHandler": "handleProjectsNotification"
},
"people": {
"enabled": true,
"localHandler": "handlePeopleNotification"
},
"items": {
"enabled": true,
"localHandler": "handleItemsNotification"
},
"communityAnalytics": {
"enabled": true,
"endpoint": "http://localhost:3001/api/analytics/community-events",
"headers": {
"Content-Type": "application/json",
"X-Privacy-Level": "aggregated"
}
}
},
"observability": {
"enableLogging": true,
"logLevel": "debug",
"enableMetrics": true,
"enableHealthChecks": true
}
}

8
test-apps/daily-notification-test/.editorconfig

@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
test-apps/daily-notification-test/.gitattributes

@ -0,0 +1 @@
* text=auto eol=lf

30
test-apps/daily-notification-test/.gitignore

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

48
test-apps/daily-notification-test/README.md

@ -0,0 +1,48 @@
# daily-notification-test
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

101
test-apps/daily-notification-test/android/.gitignore

@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
test-apps/daily-notification-test/android/app/.gitignore

@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

54
test-apps/daily-notification-test/android/app/build.gradle

@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace "com.timesafari.dailynotification.test"
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.timesafari.dailynotification.test"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

19
test-apps/daily-notification-test/android/app/capacitor.build.gradle

@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

21
test-apps/daily-notification-test/android/app/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

26
test-apps/daily-notification-test/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java

@ -0,0 +1,26 @@
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() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

73
test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- DailyNotification Plugin Components -->
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
</manifest>

5
test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/MainActivity.java

@ -0,0 +1,5 @@
package com.timesafari.dailynotification.test;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-land-hdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-land-mdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-land-xhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-land-xxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-land-xxxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-port-hdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-port-mdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-port-xhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-port-xxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable-port-xxxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

34
test-apps/daily-notification-test/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

170
test-apps/daily-notification-test/android/app/src/main/res/drawable/ic_launcher_background.xml

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

BIN
test-apps/daily-notification-test/android/app/src/main/res/drawable/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

12
test-apps/daily-notification-test/android/app/src/main/res/layout/activity_main.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

5
test-apps/daily-notification-test/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

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

5
test-apps/daily-notification-test/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

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

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
test-apps/daily-notification-test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

4
test-apps/daily-notification-test/android/app/src/main/res/values/ic_launcher_background.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

7
test-apps/daily-notification-test/android/app/src/main/res/values/strings.xml

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Daily Notification Test</string>
<string name="title_activity_main">Daily Notification Test</string>
<string name="package_name">com.timesafari.dailynotification.test</string>
<string name="custom_url_scheme">com.timesafari.dailynotification.test</string>
</resources>

22
test-apps/daily-notification-test/android/app/src/main/res/values/styles.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

5
test-apps/daily-notification-test/android/app/src/main/res/xml/file_paths.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

18
test-apps/daily-notification-test/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java

@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

29
test-apps/daily-notification-test/android/build.gradle

@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

3
test-apps/daily-notification-test/android/capacitor.settings.gradle

@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

22
test-apps/daily-notification-test/android/gradle.properties

@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

BIN
test-apps/daily-notification-test/android/gradle/wrapper/gradle-wrapper.jar

Binary file not shown.

7
test-apps/daily-notification-test/android/gradle/wrapper/gradle-wrapper.properties

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

252
test-apps/daily-notification-test/android/gradlew

@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
test-apps/daily-notification-test/android/gradlew.bat

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
test-apps/daily-notification-test/android/settings.gradle

@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

16
test-apps/daily-notification-test/android/variables.gradle

@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}

14
test-apps/daily-notification-test/capacitor.config.ts

@ -0,0 +1,14 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.timesafari.dailynotification.test',
appName: 'Daily Notification Test',
webDir: 'dist',
plugins: {
Clipboard: {
// Enable clipboard functionality
}
}
};
export default config;

1
test-apps/daily-notification-test/env.d.ts

@ -0,0 +1 @@
/// <reference types="vite/client" />

20
test-apps/daily-notification-test/eslint.config.ts

@ -0,0 +1,20 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
)

13
test-apps/daily-notification-test/index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3997
test-apps/android-test/package-lock.json → test-apps/daily-notification-test/package-lock.json

File diff suppressed because it is too large

42
test-apps/daily-notification-test/package.json

@ -0,0 +1,42 @@
{
"name": "daily-notification-test",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix"
},
"dependencies": {
"@capacitor/android": "^6.2.1",
"@capacitor/cli": "^6.2.1",
"@capacitor/core": "^6.2.1",
"date-fns": "^4.1.0",
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-facing-decorator": "^4.0.1",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.18.6",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.33.0",
"eslint-plugin-vue": "~10.4.0",
"jiti": "^2.5.1",
"npm-run-all2": "^8.0.4",
"typescript": "~5.9.0",
"vite": "^7.1.7",
"vite-plugin-vue-devtools": "^8.0.2",
"vue-tsc": "^3.1.0"
}
}

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

Loading…
Cancel
Save