From d5769208101e8d2ef0c0235fad8cb3c7c75804a3 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Mon, 3 Nov 2025 10:03:08 +0000
Subject: [PATCH 01/17] docs: add daily notification plugin integration
planning documents
- Add comprehensive integration plan following meta_feature_planning workflow
- Add AccountViewView integration strategy with PlatformService approach
- Document architecture decisions: PlatformService interface integration
- Remove web/push notification references
- Document implementation phases and acceptance criteria
---
...cation-accountview-integration-strategy.md | 472 +++++++++++++
...ly-notification-plugin-integration-plan.md | 658 ++++++++++++++++++
2 files changed, 1130 insertions(+)
create mode 100644 doc/daily-notification-accountview-integration-strategy.md
create mode 100644 doc/daily-notification-plugin-integration-plan.md
diff --git a/doc/daily-notification-accountview-integration-strategy.md b/doc/daily-notification-accountview-integration-strategy.md
new file mode 100644
index 00000000..7d7fd374
--- /dev/null
+++ b/doc/daily-notification-accountview-integration-strategy.md
@@ -0,0 +1,472 @@
+# Daily Notification Plugin - AccountViewView Integration Strategy
+
+**Author**: Matthew Raymer
+**Date**: 2025-11-03
+**Status**: 🎯 **PLANNING** - UI integration strategy
+**Feature**: Daily Notification Scheduling in Account Settings
+
+---
+
+## Overview
+
+This document outlines the strategy for integrating daily notification scheduling into `AccountViewView.vue`, allowing users to configure notification times directly from their account settings.
+
+---
+
+## Current State Analysis
+
+### Account Settings Context
+
+**Location**: `AccountViewView.vue` - Settings view for user account configuration
+
+**Integration Approach**: Add new "Daily Notifications" section for scheduling native daily notifications using PlatformService.
+
+---
+
+## Integration Strategy
+
+**Approach**: Create a separate "Daily Notifications" section
+
+This approach adds a dedicated "Daily Notifications" section that checks PlatformService capabilities. On Capacitor platforms, it provides full functionality. On other platforms, the UI is hidden when PlatformService returns `null` for notification methods.
+
+**Key Benefits**:
+- Uses PlatformService interface pattern (consistent with camera, filesystem)
+- Platform-specific features properly isolated
+- Can use native time picker (better UX on mobile)
+- Future-proof: Easy to extend with additional notification features
+- Graceful degradation on unsupported platforms
+
+**Implementation**:
+```vue
+
+
+
+ Daily Notifications
+
+
+
+
+
Daily Notification
+
+
+
+
+
+
+
+
+
+ Scheduled for: {{ nativeNotificationTime }}
+
+
+
+
+```
+
+---
+
+## Approach Rationale
+
+**Decision Date**: 2025-11-03
+**Status**: ✅ **ACCEPTED** - Will proceed with separate native notification section
+
+1. **Clear Platform Distinction**: Users understand this is for mobile apps
+2. **No Conflicts**: Doesn't interfere with disabled web notifications
+3. **Better UX**: Can use native time picker on mobile
+4. **Future-Proof**: Easy to extend with additional native notification features
+
+### Implementation Status
+
+- [x] Approach decision finalized
+- [ ] Implementation begins (Phase 1)
+
+---
+
+## UI Component Design
+
+### 1. Platform Capability Detection
+
+```typescript
+// In AccountViewView component
+async checkNotificationSupport(): Promise {
+ const platformService = PlatformServiceFactory.getInstance();
+ const status = await platformService.getDailyNotificationStatus();
+ return status !== null; // null means not supported
+}
+```
+
+### 2. State Management
+
+```typescript
+// Component properties
+nativeNotificationEnabled: boolean = false;
+nativeNotificationTime: string = ""; // Display format: "9:00 AM"
+nativeNotificationTimeStorage: string = ""; // Plugin format: "09:00"
+nativeNotificationTitle: string = "Daily Update";
+nativeNotificationMessage: string = "Your daily notification is ready!";
+```
+
+### 3. Time Input ✅ **SELECTED: HTML5 Time Input**
+
+**Decision**: Use HTML5 `` for native mobile experience
+
+```vue
+
+```
+
+**Benefits**:
+- Native mobile time picker UI on Capacitor platforms
+- Simpler implementation (no custom time parsing needed)
+- Automatic 24-hour format output (compatible with plugin)
+- System handles locale-specific time formatting
+- Better UX on mobile devices
+
+**Note**: HTML5 time input provides time in "HH:mm" format (24-hour) which matches the plugin's expected format perfectly.
+
+### 4. Time Format Conversion (Using System Time)
+
+**Key Principle**: Use device's local system time - no timezone conversions needed. The plugin handles system time natively.
+
+```typescript
+// Convert "09:00" (plugin storage format) to "9:00 AM" (display)
+function formatTimeForDisplay(time24: string): string {
+ const [hours, minutes] = time24.split(':');
+ const hourNum = parseInt(hours);
+ const isPM = hourNum >= 12;
+ const displayHour = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
+ return `${displayHour}:${minutes} ${isPM ? 'PM' : 'AM'}`;
+}
+
+// HTML5 time input provides "HH:mm" in local time - use directly
+// No UTC conversion needed - plugin handles local timezone
+function getTimeFromInput(timeInput: string): string {
+ // timeInput is already in "HH:mm" format from
+ // This is in the user's local timezone - pass directly to plugin
+ return timeInput; // e.g., "09:00" in user's local time
+}
+```
+
+**Time Handling**:
+
+- **PlatformService Integration**: Uses device's local system time directly - NO UTC conversion needed. The plugin schedules notifications on the device itself, using the device's timezone.
+
+**Implementation Principles**:
+- HTML5 `` provides time in device's local timezone
+- Plugin receives time in "HH:mm" format and schedules relative to device's local time
+- No manual timezone conversion or UTC calculations needed
+- System automatically handles:
+ - Timezone changes
+ - Daylight saving time transitions
+ - Device timezone updates
+- User sets "9:00 AM" in their local time → plugin schedules for 9:00 AM local time every day
+
+---
+
+## Data Flow
+
+### 1. Initialization
+
+```typescript
+async initializeState() {
+ // ... existing initialization ...
+
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // Check if notifications are supported on this platform
+ const status = await platformService.getDailyNotificationStatus();
+ if (status === null) {
+ // Notifications not supported - don't initialize
+ return;
+ }
+
+ // Load from settings
+ const nativeNotificationTime = settings.nativeNotificationTime || "";
+ this.nativeNotificationEnabled = !!nativeNotificationTime;
+ this.nativeNotificationTimeStorage = nativeNotificationTime;
+
+ if (nativeNotificationTime) {
+ this.nativeNotificationTime = formatTimeForDisplay(nativeNotificationTime);
+ }
+
+ // Update UI with current status
+ this.notificationStatus = status;
+}
+```
+
+### 2. Enable Notification
+
+```typescript
+async enableNativeNotification() {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // 1. Request permissions if needed
+ const permissions = await platformService.checkNotificationPermissions();
+ if (permissions === null || permissions.notifications !== 'granted') {
+ const result = await platformService.requestNotificationPermissions();
+ if (result === null || !result.notifications) {
+ throw new Error("Notification permissions denied");
+ }
+ }
+
+ // 2. Schedule notification via PlatformService
+ // Time is in device's local system time (from HTML5 time input)
+ // PlatformService handles timezone and scheduling internally
+ await platformService.scheduleDailyNotification({
+ time: this.nativeNotificationTimeStorage, // "09:00" in local time
+ title: this.nativeNotificationTitle,
+ body: this.nativeNotificationMessage,
+ sound: true,
+ priority: 'high'
+ });
+
+ // 3. Save to settings
+ await this.$saveSettings({
+ nativeNotificationTime: this.nativeNotificationTimeStorage,
+ nativeNotificationTitle: this.nativeNotificationTitle,
+ nativeNotificationMessage: this.nativeNotificationMessage,
+ });
+
+ // 4. Update UI state
+ this.nativeNotificationEnabled = true;
+
+ this.notify.success("Daily notification scheduled successfully", TIMEOUTS.SHORT);
+ } catch (error) {
+ logger.error("Failed to enable notification:", error);
+ this.notify.error("Failed to schedule notification. Please try again.", TIMEOUTS.LONG);
+ }
+}
+
+### 3. Disable Notification
+
+```typescript
+async disableNativeNotification() {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // 1. Cancel notification via PlatformService
+ await platformService.cancelDailyNotification();
+
+ // 2. Clear settings
+ await this.$saveSettings({
+ nativeNotificationTime: "",
+ nativeNotificationTitle: "",
+ nativeNotificationMessage: "",
+ });
+
+ // 3. Update UI state
+ this.nativeNotificationEnabled = false;
+ this.nativeNotificationTime = "";
+ this.nativeNotificationTimeStorage = "";
+
+ this.notify.success("Daily notification disabled", TIMEOUTS.SHORT);
+ } catch (error) {
+ logger.error("Failed to disable native notification:", error);
+ this.notify.error("Failed to disable notification. Please try again.", TIMEOUTS.LONG);
+ }
+}
+```
+
+### 4. Edit Time
+
+**Approach**: Use inline HTML5 time input for quick edits
+
+```typescript
+async editNativeNotificationTime() {
+ // Show inline HTML5 time input for quick changes
+ // For complex editing (title, message), navigate to ScheduleView
+ this.showTimeEdit = true;
+}
+```
+
+**Implementation Note**: HTML5 time input provides native mobile picker experience when shown inline, making it ideal for quick time adjustments in AccountViewView.
+
+---
+
+## Settings Schema
+
+### New Settings Fields
+
+```typescript
+// Add to Settings interface in src/db/tables/settings.ts
+interface Settings {
+ // ... existing fields ...
+
+ // Native notification settings (Capacitor only)
+ nativeNotificationTime?: string; // "09:00" format (24-hour)
+ nativeNotificationTitle?: string; // Default: "Daily Update"
+ nativeNotificationMessage?: string; // Default message
+}
+```
+
+### Settings Persistence
+
+- Store in `settings` table via `$saveSettings()`
+- Use same pattern as `notifyingNewActivityTime`
+- Persist across app restarts
+- Sync with plugin state on component mount
+
+---
+
+## Plugin Integration
+
+### PlatformService Usage
+
+```typescript
+import { PlatformServiceFactory } from '@/services/PlatformServiceFactory';
+
+const platformService = PlatformServiceFactory.getInstance();
+
+// Check if notifications are supported
+const status = await platformService.getDailyNotificationStatus();
+if (status === null) {
+ // Notifications not supported on this platform
+}
+```
+
+### Key Operations (via PlatformService)
+
+1. **Check Status**: `getDailyNotificationStatus()` - Returns status or `null` if unsupported
+2. **Check Permissions**: `checkNotificationPermissions()` - Returns permissions or `null` if unsupported
+3. **Request Permissions**: `requestNotificationPermissions()` - Requests permissions or returns `null` if unsupported
+4. **Schedule**: `scheduleDailyNotification(options)` - Schedules notification or throws error if unsupported
+5. **Cancel**: `cancelDailyNotification()` - Cancels notification or throws error if unsupported
+
+---
+
+## UI/UX Considerations
+
+### Visual Design
+- **Section Style**: Match existing notification section (`bg-slate-100 rounded-md`)
+- **Toggle Switch**: Reuse existing custom toggle pattern
+- **Time Display**: Show in user-friendly format ("9:00 AM")
+- **Edit Button**: Small, subtle link/button to edit time
+
+### User Feedback
+- **Success**: Toast notification when scheduled successfully
+- **Error**: Clear error message with troubleshooting guidance
+- **Loading**: Show loading state during plugin operations
+- **Permission Request**: Handle gracefully if denied
+
+### Accessibility
+- **ARIA Labels**: Proper labels for all interactive elements
+- **Keyboard Navigation**: Full keyboard support
+- **Screen Reader**: Clear announcements for state changes
+
+---
+
+## Implementation Phases
+
+### Phase 1: Basic Integration
+- [ ] Add platform detection property
+- [ ] Create native notification section in template
+- [ ] Add component state properties
+- [ ] Implement toggle functionality
+- [ ] Basic enable/disable operations
+
+### Phase 2: Time Management
+- [ ] Add time input (HTML5 time picker)
+- [ ] Implement time format conversion
+- [ ] Add edit time functionality
+- [ ] Save/load time from settings
+
+### Phase 3: Plugin Integration
+- [ ] Integrate with DailyNotificationFactory
+- [ ] Schedule notification on enable
+- [ ] Cancel notification on disable
+- [ ] Update notification on time change
+- [ ] Sync plugin state with settings
+
+### Phase 4: Polish & Error Handling
+- [ ] Permission request flow
+- [ ] Error handling and user feedback
+- [ ] Status verification
+- [ ] Help/info dialogs
+- [ ] Accessibility improvements
+
+---
+
+## Implementation Decisions
+
+### Time Input Format ✅
+- **Selected**: HTML5 `` for Capacitor platforms
+- **Rationale**: Native mobile experience, simpler code, automatic 24-hour format
+
+### Edit Approach ✅
+- **Selected**: Inline HTML5 time input for quick edits in AccountViewView
+- **Note**: For complex editing (title, message changes), users can navigate to dedicated ScheduleView
+
+### Settings Field Names ✅
+- **Selected**: `nativeNotificationTime`, `nativeNotificationTitle`, `nativeNotificationMessage`
+- **Rationale**: Clear distinction from web push notification fields
+
+### Notification Title/Message ✅
+- **Selected**: Allow customization, default to "Daily Update" / "Your daily notification is ready!"
+- **Rationale**: Flexibility for users, sensible defaults
+
+---
+
+## Success Criteria
+
+- [ ] Native notification section appears only on Capacitor platforms
+- [ ] Toggle enables/disables notifications via plugin
+- [ ] Time can be set and edited
+- [ ] Settings persist across app restarts
+- [ ] Plugin state syncs with settings
+- [ ] Error handling provides clear user feedback
+- [ ] UI matches existing design patterns
+- [ ] Accessibility requirements met
+
+---
+
+## Related Components
+
+- **PlatformService**: Interface for platform capabilities (notification methods)
+- **PlatformServiceFactory**: Factory for getting platform service instance
+- **ScheduleView**: Dedicated scheduling interface (for complex editing)
+- **AccountViewView**: Main settings view (integration target)
+
+---
+
+## Next Steps
+
+1. ~~**Decide on Approach**: Separate native notification section~~ ✅ **DECIDED**
+2. **Define Settings Schema**: Add native notification fields to Settings interface
+3. **Create UI Components**: Build notification section in AccountViewView
+4. **Integrate Plugin**: Connect UI to DailyNotificationFactory service
+5. **Test Flow**: Verify enable/disable/edit workflows
+6. **Add Help Content**: Create help documentation for native notifications
+
+---
+
+**See also**:
+- `doc/daily-notification-plugin-integration-plan.md` - Overall integration plan
+- `src/views/AccountViewView.vue` - Target component for integration
+- `src/services/PlatformService.ts` - PlatformService interface definition
+- `src/services/PlatformServiceFactory.ts` - Factory for platform service instances
+
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
new file mode 100644
index 00000000..ba07104a
--- /dev/null
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -0,0 +1,658 @@
+# Daily Notification Plugin Integration Plan
+
+**Author**: Matthew Raymer
+**Date**: 2025-11-03
+**Status**: 🎯 **PLANNING** - Feature planning phase
+**Feature**: Daily Notification Plugin Integration
+**Platform Scope**: Capacitor-only (Android/iOS)
+
+---
+
+## Executive Summary
+
+This plan outlines the integration of `@timesafari/daily-notification-plugin` into the TimeSafari application using the PlatformService interface pattern. The feature is implemented on all platforms via PlatformService, but only Capacitor platforms provide full functionality. Web and Electron platforms return `null` for unsupported operations.
+
+### Key Requirements
+
+- **Platform**: All platforms (Capacitor provides full functionality, Web/Electron return null)
+- **Architecture**: PlatformService interface integration (all platforms implement, unsupported return null)
+- **Components**: Home view (diagnostics/status) + Schedule view (time setting)
+- **Store**: Pinia store for notification state management
+- **Routes**: New routes for schedule, notifications, history, settings views
+
+---
+
+## Complexity Assessment
+
+### Technical Complexity: **Medium**
+
+#### Code Changes
+- **Medium**: New Vue components, Pinia store, router routes
+- **Pattern**: Following PlatformService interface pattern (like camera, filesystem methods) - all platforms implement, unsupported return null
+- **Integration**: Plugin API integration with error handling
+
+#### Platform Impact
+- **Single Platform**: Capacitor-only (Android/iOS)
+- **Conditional Loading**: Feature only loads on Capacitor platforms
+- **Graceful Degradation**: Web/Electron builds should not break when plugin unavailable
+
+#### Testing Requirements
+- **Comprehensive**:
+ - Plugin availability detection
+ - Permission request flows
+ - Notification scheduling
+ - Status checking
+ - Cross-platform validation (ensure web/electron unaffected)
+
+### Dependency Complexity
+
+#### Internal Dependencies
+- **Medium**:
+ - Router configuration (new routes)
+ - Store creation (Pinia)
+ - Component dependencies (ActionCard, StatusCard)
+ - Logger integration (replace console.* with project logger)
+
+#### External Dependencies
+- **Medium**:
+ - `@timesafari/daily-notification-plugin` (external package)
+ - Capacitor core APIs
+ - Platform detection utilities
+
+#### Infrastructure Dependencies
+- **Low**:
+ - Package.json update (add plugin dependency)
+ - Vite conditional imports for Capacitor builds only
+ - No infrastructure changes required
+
+### Risk Factors
+
+1. **Plugin Availability**: Plugin may not be available in package registry
+ - **Mitigation**: Verify package availability, consider local development setup
+
+2. **Platform Implementation**: All platforms must implement interface methods
+ - **Mitigation**: Follow PlatformService pattern - Capacitor provides full implementation, Web/Electron return null or throw errors
+
+3. **Web/Electron Compatibility**: Feature must not break non-Capacitor builds
+ - **Mitigation**: Use dynamic imports with platform checks, graceful fallbacks
+
+4. **Store State Management**: Notification state persistence
+ - **Mitigation**: Follow existing Pinia patterns in codebase
+
+---
+
+## Platform Analysis
+
+### Target Platform: Capacitor Only
+
+#### Capacitor Requirements
+- Android: API 21+ (already supported)
+- iOS: 13+ (already supported)
+- Native platform detection: `Capacitor.isNativePlatform()`
+
+#### Build Configuration
+- **Vite Config**: `vite.config.capacitor.mts` (already exists)
+- **Build Command**: `npm run build:capacitor`
+- **Conditional Import Pattern**: Dynamic import based on `process.env.VITE_PLATFORM === 'capacitor'`
+
+#### Platform Detection Strategy
+
+**Pattern**: PlatformService interface - all platforms implement methods
+
+Components check PlatformService capabilities by calling methods and checking for `null` returns:
+
+```typescript
+// Components check capability via PlatformService
+const platformService = PlatformServiceFactory.getInstance();
+const status = await platformService.getDailyNotificationStatus();
+
+if (status === null) {
+ // Notifications not supported on this platform - hide UI
+ return;
+}
+// Continue with notification features
+```
+
+**Why PlatformService Pattern?**
+- Consistent with existing platform capabilities (camera, filesystem)
+- All platforms implement the interface (contract compliance)
+- Unsupported platforms return `null` or throw clear errors
+- Components handle capability detection via method results, not environment variables
+
+### Web/Electron Implementation Strategy
+
+#### Web Platform
+- **Implementation**: All notification methods implemented in `WebPlatformService`
+- **Behavior**: Methods return `null` for status/permissions, throw errors for scheduling
+- **UI**: Components check for `null` responses to hide notification UI
+- **Plugin Import**: No plugin imports - methods return null/throw errors directly
+
+#### Electron Platform
+- **Implementation**: All notification methods implemented in `ElectronPlatformService`
+- **Behavior**: Methods return `null` for status/permissions, throw errors for scheduling
+- **UI**: Components check for `null` responses to hide notification UI
+- **Plugin Import**: No plugin imports - methods return null/throw errors directly
+
+---
+
+## Architecture Design
+
+### PlatformService Integration
+
+**Key Pattern**: Add notification methods directly to `PlatformService` interface, implemented on all platforms. Unsupported platforms return `null` or empty results.
+
+This follows the same pattern as other platform capabilities (camera, filesystem) where all platforms implement the interface, but unsupported platforms return null/empty results.
+
+```typescript
+// src/services/PlatformService.ts - Add to interface
+export interface PlatformService {
+ // ... existing methods ...
+
+ // Daily notification operations
+ /**
+ * Get the status of scheduled daily notifications
+ * @returns Promise resolving to notification status, or null if not supported
+ */
+ getDailyNotificationStatus(): Promise;
+
+ /**
+ * Check notification permissions
+ * @returns Promise resolving to permission status, or null if not supported
+ */
+ checkNotificationPermissions(): Promise;
+
+ /**
+ * Request notification permissions
+ * @returns Promise resolving to permission result, or null if not supported
+ */
+ requestNotificationPermissions(): Promise;
+
+ /**
+ * Schedule a daily notification
+ * @param options - Notification scheduling options
+ * @returns Promise that resolves when scheduled, or rejects if not supported
+ */
+ scheduleDailyNotification(options: ScheduleOptions): Promise;
+
+ /**
+ * Cancel scheduled daily notification
+ * @returns Promise that resolves when cancelled, or rejects if not supported
+ */
+ cancelDailyNotification(): Promise;
+
+ /**
+ * Configure native fetcher for background operations
+ * @param config - Native fetcher configuration
+ * @returns Promise that resolves when configured, or null if not supported
+ */
+ configureNativeFetcher(config: NativeFetcherConfig): Promise;
+
+ /**
+ * Update starred plans for background fetcher
+ * @param plans - Starred plan IDs
+ * @returns Promise that resolves when updated, or null if not supported
+ */
+ updateStarredPlans(plans: { planIds: string[] }): Promise;
+}
+```
+
+**Implementation Pattern**:
+- **CapacitorPlatformService**: Full implementation using `@timesafari/daily-notification-plugin`
+- **WebPlatformService**: Returns `null` for status/permissions, throws errors for scheduling operations
+- **ElectronPlatformService**: Returns `null` for status/permissions, throws errors for scheduling operations
+
+### PlatformService Interface Extensions
+
+```typescript
+// Types/interfaces for notification operations
+export interface NotificationStatus {
+ isScheduled: boolean;
+ scheduledTime?: string; // "HH:mm" format
+ lastTriggered?: string;
+ permissions: PermissionStatus;
+}
+
+export interface PermissionStatus {
+ notifications: 'granted' | 'denied' | 'prompt';
+ exactAlarms?: 'granted' | 'denied' | 'prompt'; // Android only
+}
+
+export interface PermissionResult {
+ notifications: boolean;
+ exactAlarms?: boolean; // Android only
+}
+
+export interface ScheduleOptions {
+ time: string; // "HH:mm" format in local time
+ title: string;
+ body: string;
+ sound?: boolean;
+ priority?: 'high' | 'normal' | 'low';
+}
+
+export interface NativeFetcherConfig {
+ apiServer: string;
+ jwt: string;
+ starredPlanHandleIds: string[];
+}
+```
+
+**Implementation Behavior**:
+- **Capacitor**: Full implementation, all methods functional
+- **Web/Electron**: Status/permission methods return `null`, scheduling methods throw errors with clear messages
+
+### Component Architecture
+
+#### Views Structure
+```
+src/views/
+ ├── HomeView.vue (existing - modify to add notification diagnostics)
+ ├── ScheduleView.vue (new - notification scheduling)
+ ├── NotificationsView.vue (new - view scheduled notifications)
+ ├── NotificationHistoryView.vue (new - notification history)
+ └── NotificationSettingsView.vue (new - notification settings)
+```
+
+#### Supporting Components
+```
+src/components/cards/
+ ├── ActionCard.vue (new - reusable action card)
+ └── StatusCard.vue (new - reusable status card)
+```
+
+#### Store Structure
+```
+src/stores/
+ └── app.ts (new - Pinia store for app-wide state)
+ - notificationStatus: NotificationStatus | null
+ - platform: 'web' | 'capacitor' | 'electron'
+ - setNotificationStatus(status): void
+```
+
+### Router Integration
+
+```typescript
+// src/router/index.ts - Add new routes
+{
+ path: "/schedule",
+ name: "schedule",
+ component: () => import("../views/ScheduleView.vue"),
+},
+{
+ path: "/notifications",
+ name: "notifications",
+ component: () => import("../views/NotificationsView.vue"),
+},
+{
+ path: "/history",
+ name: "notification-history",
+ component: () => import("../views/NotificationHistoryView.vue"),
+},
+{
+ path: "/settings",
+ name: "settings",
+ component: () => import("../views/NotificationSettingsView.vue"),
+},
+```
+
+---
+
+## Phase Breakdown
+
+### Phase 1: Foundation & Infrastructure
+
+**Complexity**: Low-Medium
+**Goals**: Set up factory architecture, store, and conditional loading
+
+#### Tasks
+1. **Package Dependency**
+ - Add `@timesafari/daily-notification-plugin` to `package.json`
+ - Verify package availability/version
+ - Document in dependencies section
+
+2. **PlatformService Interface Extension**
+ - Add notification methods to `PlatformService` interface
+ - Define notification types/interfaces (NotificationStatus, ScheduleOptions, etc.)
+ - Implement in `CapacitorPlatformService` using `@timesafari/daily-notification-plugin`
+ - Implement in `WebPlatformService` with null returns / error throws
+ - Implement in `ElectronPlatformService` with null returns / error throws
+
+3. **Pinia Store Setup**
+ - Create `src/stores/app.ts` with notification state
+ - Define `NotificationStatus` interface
+ - Implement `setNotificationStatus()` action
+ - Add platform detection to store
+
+4. **Native Fetcher Configuration Integration**
+ - Update HomeView `configureNativeFetcher()` to use active DID management
+ - Replace `TEST_USER_ZERO_CONFIG` references with `$getActiveIdentity()`
+ - Replace `generateEndorserJWT` with `createEndorserJwtForDid()` from `src/libs/endorserServer.ts`
+ - Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
+
+5. **Router Routes**
+ - Add route definitions for schedule, notifications, history, settings
+ - Test route navigation
+
+#### Acceptance Criteria
+- [ ] PlatformService interface extended with notification methods
+- [ ] CapacitorPlatformService implements notification methods using plugin
+- [ ] WebPlatformService and ElectronPlatformService return null/throw errors appropriately
+- [ ] Pinia store created and tested
+- [ ] HomeView `configureNativeFetcher()` updated to use active DID (no TEST_USER_ZERO_CONFIG)
+- [ ] Routes added and accessible
+- [ ] No build errors in web/electron builds
+
+---
+
+### Phase 2: Core Components
+
+**Complexity**: Medium
+**Goals**: Create reusable components and main views
+
+#### Tasks
+1. **Reusable Components**
+ - Create `ActionCard.vue` component
+ - Create `StatusCard.vue` component
+ - Follow project styling patterns
+ - Add TypeScript interfaces
+
+2. **Home View Integration**
+ - Modify existing `HomeView.vue` OR create new notification home view
+ - Integrate plugin diagnostics
+ - Add system status display
+ - Connect to Pinia store
+ - Replace `console.*` with project logger
+
+3. **Schedule View**
+ - Create `ScheduleView.vue` (provided code as reference)
+ - Integrate with PlatformService via PlatformServiceFactory
+ - Add error handling
+ - Replace `console.*` with project logger
+ - Add loading states
+
+4. **AccountViewView Integration** ✅ **ACCEPTED: Option A**
+ - Add separate "Daily Notifications" section
+ - Check platform capabilities before showing UI
+ - Add toggle switch for enabling/disabling notifications
+ - Add HTML5 time input for scheduling time
+ - Integrate with PlatformService via PlatformServiceFactory
+ - Save/load settings from `settings` table
+
+#### Acceptance Criteria
+- [ ] ActionCard and StatusCard components created
+- [ ] Home view shows notification diagnostics
+- [ ] Schedule view allows notification scheduling
+- [ ] AccountViewView has separate "Daily Notifications" section (Option A accepted)
+- [ ] Notification section checks PlatformService capabilities before showing
+- [ ] Toggle and time input functional in AccountViewView
+- [ ] All logging uses project logger
+- [ ] Error handling implemented
+- [ ] Loading states visible
+
+---
+
+### Phase 3: Supporting Views & Configuration
+
+**Complexity**: Medium
+**Goals**: Complete all views and native fetcher configuration
+
+#### Tasks
+1. **Supporting Views**
+ - Create `NotificationsView.vue` (list scheduled notifications)
+ - Create `NotificationHistoryView.vue` (notification history)
+ - Create `NotificationSettingsView.vue` (settings/preferences)
+
+2. **Native Fetcher Configuration**
+ - Integrate `configureNativeFetcher()` in HomeView
+ - Use `$getActiveIdentity()` to get active DID (replace TEST_USER_ZERO_CONFIG)
+ - Use `createEndorserJwtForDid()` for JWT generation
+ - Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
+ - Add error handling for configuration failures
+
+3. **Permission Management**
+ - Implement permission request flow
+ - Handle permission denial gracefully
+ - Update status after permission changes
+
+#### Acceptance Criteria
+- [ ] All supporting views created and functional
+- [ ] Native fetcher configuration working
+- [ ] Permission requests handled properly
+- [ ] Status updates after permission changes
+- [ ] Error handling for all failure cases
+
+---
+
+### Phase 4: Testing & Validation
+
+**Complexity**: Medium-High
+**Goals**: Comprehensive testing across platforms and scenarios
+
+#### Tasks
+1. **Capacitor Testing**
+ - Test plugin availability detection
+ - Test notification scheduling on Android
+ - Test notification scheduling on iOS
+ - Test permission requests
+ - Test status updates
+ - Test native fetcher configuration
+
+2. **Cross-Platform Validation**
+ - Verify web build doesn't break
+ - Verify Electron build doesn't break
+ - Verify feature is hidden on non-Capacitor platforms
+ - Test graceful degradation
+
+3. **Integration Testing**
+ - Test full scheduling workflow
+ - Test status checking workflow
+ - Test navigation between views
+ - Test store state persistence
+
+4. **Error Scenarios**
+ - Test plugin unavailable scenarios
+ - Test permission denied scenarios
+ - Test network failures (for native fetcher)
+ - Test invalid configuration scenarios
+
+#### Acceptance Criteria
+- [ ] All Capacitor tests passing
+- [ ] Web/Electron builds unaffected
+- [ ] Integration tests passing
+- [ ] Error scenarios handled gracefully
+- [ ] Documentation updated
+
+---
+
+## Milestones
+
+### Milestone 1: Foundation Complete
+**Success Criteria**:
+- Factory service operational
+- Store created and tested
+- Routes accessible
+- No build regressions
+
+### Milestone 2: Core Features Operational
+**Success Criteria**:
+- Home view shows diagnostics
+- Schedule view functional
+- Plugin integration working
+- Logging standardized
+
+### Milestone 3: Full Feature Set
+**Success Criteria**:
+- All views created and functional
+- Native fetcher configured
+- Permissions managed properly
+- Status updates working
+
+### Milestone 4: Production Ready
+**Success Criteria**:
+- All tests passing
+- Cross-platform validation complete
+- Error handling robust
+- Documentation complete
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+- Factory service platform detection
+- Store actions and state management
+- Component rendering and interactions
+
+### Integration Tests
+- Plugin API calls
+- Permission flows
+- Status updates
+- Navigation between views
+
+### Platform Tests
+- **Capacitor Android**: Notification scheduling, permissions, status
+- **Capacitor iOS**: Notification scheduling, permissions, status
+- **Web**: Feature hidden, no errors
+- **Electron**: Feature hidden, no errors
+
+### E2E Tests (Playwright)
+- Full notification scheduling workflow
+- Permission request flow
+- Status checking workflow
+- Error handling scenarios
+
+---
+
+## Dependencies
+
+### External Dependencies
+- `@timesafari/daily-notification-plugin` (to be added)
+- `@capacitor/core` (already in project)
+- `vue` (already in project)
+- `vue-router` (already in project)
+- `pinia` (already in project)
+
+### Internal Dependencies
+- Logger service (`@/utils/logger`)
+- Platform detection utilities
+- Router configuration
+- Existing component patterns
+
+### Configuration Dependencies
+- **Active DID Management**: Use `$getActiveIdentity()` from `PlatformServiceMixin` (existing)
+- **JWT Generation**: Use `createEndorserJwtForDid(activeDid, payload)` from `src/libs/endorserServer.ts` (existing)
+- **Settings Access**: Use `$accountSettings()` for `apiServer` and `starredPlanHandleIds` (existing)
+- **No new config files needed**: Replace `TEST_USER_ZERO_CONFIG` references with active DID and settings
+
+---
+
+## Implementation Notes
+
+### Code Quality Standards
+- **Logging**: Use `logger` from `@/utils/logger`, not `console.*`
+- **File Documentation**: Add file-level documentation headers
+- **Method Documentation**: Rich method-level documentation
+- **Type Safety**: Full TypeScript typing
+- **PEP8/Prettier**: Follow code style guidelines
+- **Line Length**: Keep methods < 80 columns when possible
+
+### Architecture Patterns to Follow
+- **Factory Pattern**: Like `QRScannerFactory` for conditional loading
+- **Service Interface**: Abstract interface with platform implementations
+- **Store Pattern**: Pinia store for state management
+- **Composition API vs Class**: Use provided code style (Composition API for HomeView, Class for ScheduleView)
+
+### PlatformService Integration Strategy
+
+**Pattern**: Direct integration into PlatformService interface (like camera, filesystem methods)
+
+```typescript
+// In components - use PlatformServiceFactory pattern
+import { PlatformServiceFactory } from '@/services/PlatformServiceFactory';
+
+const platformService = PlatformServiceFactory.getInstance();
+
+// Check if notifications are supported
+const status = await platformService.getDailyNotificationStatus();
+if (status === null) {
+ // Notifications not supported on this platform - hide UI
+ return;
+}
+
+// Schedule notification
+await platformService.scheduleDailyNotification({
+ time: "09:00",
+ title: "Daily Update",
+ body: "Your daily notification is ready!",
+});
+```
+
+**Key Points**:
+- Methods available on all PlatformService implementations
+- CapacitorPlatformService provides full implementation
+- WebPlatformService/ElectronPlatformService return `null` or throw errors
+- Components check for `null` responses to hide/show UI appropriately
+- No separate factory needed - uses existing PlatformServiceFactory pattern
+
+---
+
+## Risk Mitigation
+
+### Risk 1: Plugin Package Unavailable
+**Mitigation**:
+- Verify package exists and is accessible
+- Consider local development setup if needed
+- Document package installation requirements
+
+### Risk 2: Platform Detection Failures
+**Mitigation**:
+- Use proven patterns from `QRScannerFactory`
+- Test on all platforms
+- Add fallback logic
+
+### Risk 3: Web/Electron Build Breaks
+**Mitigation**:
+- Use dynamic imports exclusively
+- Test web/electron builds after each phase
+- Ensure no static plugin imports
+
+### Risk 4: Configuration Dependencies (RESOLVED)
+**Mitigation**:
+- **Use existing active DID management**: Use `$getActiveIdentity()` from `PlatformServiceMixin` to get currently selected DID
+- **Use existing JWT generation**: Use `createEndorserJwtForDid(activeDid, payload)` from `src/libs/endorserServer.ts`
+- **Use existing settings**: Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
+- **No config files needed**: The HomeView component code references `TEST_USER_ZERO_CONFIG`, but should instead use the currently active DID and settings
+
+---
+
+## Success Criteria Summary
+
+- [ ] Plugin integrated using factory architecture
+- [ ] Feature works on Capacitor (Android/iOS)
+- [ ] Feature hidden/graceful on Web/Electron
+- [ ] All components created and functional
+- [ ] Store manages notification state
+- [ ] Router routes accessible
+- [ ] Logging standardized (no console.*)
+- [ ] Error handling robust
+- [ ] Cross-platform testing complete
+- [ ] Documentation updated
+- [ ] No build regressions
+
+---
+
+## Next Steps
+
+1. **Verify Plugin Package**: Confirm `@timesafari/daily-notification-plugin` availability
+2. **Update HomeView Configuration**: Replace `TEST_USER_ZERO_CONFIG` references in HomeView with existing active DID management (`$getActiveIdentity()`, `createEndorserJwtForDid()`, `$accountSettings()`)
+3. **Extend PlatformService**: Add notification methods to PlatformService interface and implement in all platform services
+4. **Set Up Store**: Create Pinia store for notification state
+5. **Begin Phase 1 Implementation**: Start with foundation tasks
+
+---
+
+**See also**:
+- `.cursor/rules/meta_feature_planning.mdc` - Feature planning workflow
+- `.cursor/rules/app/architectural_patterns.mdc` - Architecture patterns
+- `.cursor/rules/app/timesafari_platforms.mdc` - Platform requirements
+- `src/services/QRScanner/QRScannerFactory.ts` - Factory pattern reference
--
2.30.2
From f9446f529b19bb862dcff6142b15d723b5365064 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Mon, 3 Nov 2025 10:03:17 +0000
Subject: [PATCH 02/17] chore: add @timesafari/daily-notification-plugin
dependency
- Add daily-notification-plugin as local file dependency
- Update package-lock.json with plugin dependency tree
---
package-lock.json | 42 ++++++++++++++++++++++++++++++++++++++++++
package.json | 1 +
2 files changed, 43 insertions(+)
diff --git a/package-lock.json b/package-lock.json
index 914004eb..2ea0bbaa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.0",
+ "@timesafari/daily-notification-plugin": "file:../daily-notification-plugin",
"@tweenjs/tween.js": "^21.1.1",
"@types/qrcode": "^1.5.5",
"@veramo/core": "^5.6.0",
@@ -149,6 +150,43 @@
"vite": "^5.2.0"
}
},
+ "../daily-notification-plugin": {
+ "name": "@timesafari/daily-notification-plugin",
+ "version": "1.0.0",
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@capacitor/core": "^6.2.1"
+ },
+ "devDependencies": {
+ "@capacitor/android": "^6.2.1",
+ "@capacitor/cli": "^6.2.1",
+ "@capacitor/ios": "^6.2.1",
+ "@types/jest": "^29.5.0",
+ "@types/jsdom": "^21.1.7",
+ "@types/node": "^20.19.0",
+ "@typescript-eslint/eslint-plugin": "^5.57.0",
+ "@typescript-eslint/parser": "^5.57.0",
+ "eslint": "^8.37.0",
+ "jest": "^29.5.0",
+ "jest-environment-jsdom": "^30.0.5",
+ "jsdom": "^26.1.0",
+ "markdownlint-cli2": "^0.18.1",
+ "prettier": "^2.8.7",
+ "rimraf": "^4.4.0",
+ "rollup": "^3.20.0",
+ "rollup-plugin-typescript2": "^0.31.0",
+ "standard-version": "^9.5.0",
+ "ts-jest": "^29.1.0",
+ "typescript": "~5.2.0",
+ "vite": "^7.1.9"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@0no-co/graphql.web": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz",
@@ -9605,6 +9643,10 @@
"node": ">=10"
}
},
+ "node_modules/@timesafari/daily-notification-plugin": {
+ "resolved": "../daily-notification-plugin",
+ "link": true
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
diff --git a/package.json b/package.json
index a9587886..57a8a6f6 100644
--- a/package.json
+++ b/package.json
@@ -166,6 +166,7 @@
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.0",
+ "@timesafari/daily-notification-plugin": "file:../daily-notification-plugin",
"@tweenjs/tween.js": "^21.1.1",
"@types/qrcode": "^1.5.5",
"@veramo/core": "^5.6.0",
--
2.30.2
From 1f858fa1ce3b921520c0161fc34b09811c6820d0 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Mon, 3 Nov 2025 10:03:25 +0000
Subject: [PATCH 03/17] build: configure daily notification plugin for
Capacitor Android
- Add plugin project to capacitor.build.gradle
- Register plugin in capacitor.plugins.json
- Include plugin project in capacitor.settings.gradle
---
android/app/capacitor.build.gradle | 1 +
android/app/src/main/assets/capacitor.plugins.json | 4 ++++
android/capacitor.settings.gradle | 3 +++
3 files changed, 8 insertions(+)
diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index f1774d9f..820cd36a 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -18,6 +18,7 @@ dependencies {
implementation project(':capacitor-share')
implementation project(':capacitor-status-bar')
implementation project(':capawesome-capacitor-file-picker')
+ implementation project(':timesafari-daily-notification-plugin')
}
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index 72f18d8c..721bea0d 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -34,5 +34,9 @@
{
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
+ },
+ {
+ "pkg": "@timesafari/daily-notification-plugin",
+ "classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
}
]
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index 891b5455..0319aa38 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -28,3 +28,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
include ':capawesome-capacitor-file-picker'
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
+
+include ':timesafari-daily-notification-plugin'
+project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')
--
2.30.2
From ced8248436acab5dac33d8b8f6d818452c625b43 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 04:01:50 +0000
Subject: [PATCH 04/17] docs: merge AccountViewView integration strategy into
main plan
- Consolidate AccountViewView integration strategy into unified plan
- Add comprehensive AccountViewView Integration Strategy section
- Include UI component design, data flow, and implementation decisions
- Remove separate strategy document to follow meta_feature_planning structure
- Update Phase 2 to include AccountViewView integration tasks
---
...cation-accountview-integration-strategy.md | 472 ------------------
...ly-notification-plugin-integration-plan.md | 401 ++++++++++++++-
2 files changed, 388 insertions(+), 485 deletions(-)
delete mode 100644 doc/daily-notification-accountview-integration-strategy.md
diff --git a/doc/daily-notification-accountview-integration-strategy.md b/doc/daily-notification-accountview-integration-strategy.md
deleted file mode 100644
index 7d7fd374..00000000
--- a/doc/daily-notification-accountview-integration-strategy.md
+++ /dev/null
@@ -1,472 +0,0 @@
-# Daily Notification Plugin - AccountViewView Integration Strategy
-
-**Author**: Matthew Raymer
-**Date**: 2025-11-03
-**Status**: 🎯 **PLANNING** - UI integration strategy
-**Feature**: Daily Notification Scheduling in Account Settings
-
----
-
-## Overview
-
-This document outlines the strategy for integrating daily notification scheduling into `AccountViewView.vue`, allowing users to configure notification times directly from their account settings.
-
----
-
-## Current State Analysis
-
-### Account Settings Context
-
-**Location**: `AccountViewView.vue` - Settings view for user account configuration
-
-**Integration Approach**: Add new "Daily Notifications" section for scheduling native daily notifications using PlatformService.
-
----
-
-## Integration Strategy
-
-**Approach**: Create a separate "Daily Notifications" section
-
-This approach adds a dedicated "Daily Notifications" section that checks PlatformService capabilities. On Capacitor platforms, it provides full functionality. On other platforms, the UI is hidden when PlatformService returns `null` for notification methods.
-
-**Key Benefits**:
-- Uses PlatformService interface pattern (consistent with camera, filesystem)
-- Platform-specific features properly isolated
-- Can use native time picker (better UX on mobile)
-- Future-proof: Easy to extend with additional notification features
-- Graceful degradation on unsupported platforms
-
-**Implementation**:
-```vue
-
-
-
- Daily Notifications
-
-
-
-
-
Daily Notification
-
-
-
-
-
-
-
-
-
- Scheduled for: {{ nativeNotificationTime }}
-
-
-
-
-```
-
----
-
-## Approach Rationale
-
-**Decision Date**: 2025-11-03
-**Status**: ✅ **ACCEPTED** - Will proceed with separate native notification section
-
-1. **Clear Platform Distinction**: Users understand this is for mobile apps
-2. **No Conflicts**: Doesn't interfere with disabled web notifications
-3. **Better UX**: Can use native time picker on mobile
-4. **Future-Proof**: Easy to extend with additional native notification features
-
-### Implementation Status
-
-- [x] Approach decision finalized
-- [ ] Implementation begins (Phase 1)
-
----
-
-## UI Component Design
-
-### 1. Platform Capability Detection
-
-```typescript
-// In AccountViewView component
-async checkNotificationSupport(): Promise {
- const platformService = PlatformServiceFactory.getInstance();
- const status = await platformService.getDailyNotificationStatus();
- return status !== null; // null means not supported
-}
-```
-
-### 2. State Management
-
-```typescript
-// Component properties
-nativeNotificationEnabled: boolean = false;
-nativeNotificationTime: string = ""; // Display format: "9:00 AM"
-nativeNotificationTimeStorage: string = ""; // Plugin format: "09:00"
-nativeNotificationTitle: string = "Daily Update";
-nativeNotificationMessage: string = "Your daily notification is ready!";
-```
-
-### 3. Time Input ✅ **SELECTED: HTML5 Time Input**
-
-**Decision**: Use HTML5 `` for native mobile experience
-
-```vue
-
-```
-
-**Benefits**:
-- Native mobile time picker UI on Capacitor platforms
-- Simpler implementation (no custom time parsing needed)
-- Automatic 24-hour format output (compatible with plugin)
-- System handles locale-specific time formatting
-- Better UX on mobile devices
-
-**Note**: HTML5 time input provides time in "HH:mm" format (24-hour) which matches the plugin's expected format perfectly.
-
-### 4. Time Format Conversion (Using System Time)
-
-**Key Principle**: Use device's local system time - no timezone conversions needed. The plugin handles system time natively.
-
-```typescript
-// Convert "09:00" (plugin storage format) to "9:00 AM" (display)
-function formatTimeForDisplay(time24: string): string {
- const [hours, minutes] = time24.split(':');
- const hourNum = parseInt(hours);
- const isPM = hourNum >= 12;
- const displayHour = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
- return `${displayHour}:${minutes} ${isPM ? 'PM' : 'AM'}`;
-}
-
-// HTML5 time input provides "HH:mm" in local time - use directly
-// No UTC conversion needed - plugin handles local timezone
-function getTimeFromInput(timeInput: string): string {
- // timeInput is already in "HH:mm" format from
- // This is in the user's local timezone - pass directly to plugin
- return timeInput; // e.g., "09:00" in user's local time
-}
-```
-
-**Time Handling**:
-
-- **PlatformService Integration**: Uses device's local system time directly - NO UTC conversion needed. The plugin schedules notifications on the device itself, using the device's timezone.
-
-**Implementation Principles**:
-- HTML5 `` provides time in device's local timezone
-- Plugin receives time in "HH:mm" format and schedules relative to device's local time
-- No manual timezone conversion or UTC calculations needed
-- System automatically handles:
- - Timezone changes
- - Daylight saving time transitions
- - Device timezone updates
-- User sets "9:00 AM" in their local time → plugin schedules for 9:00 AM local time every day
-
----
-
-## Data Flow
-
-### 1. Initialization
-
-```typescript
-async initializeState() {
- // ... existing initialization ...
-
- const platformService = PlatformServiceFactory.getInstance();
-
- // Check if notifications are supported on this platform
- const status = await platformService.getDailyNotificationStatus();
- if (status === null) {
- // Notifications not supported - don't initialize
- return;
- }
-
- // Load from settings
- const nativeNotificationTime = settings.nativeNotificationTime || "";
- this.nativeNotificationEnabled = !!nativeNotificationTime;
- this.nativeNotificationTimeStorage = nativeNotificationTime;
-
- if (nativeNotificationTime) {
- this.nativeNotificationTime = formatTimeForDisplay(nativeNotificationTime);
- }
-
- // Update UI with current status
- this.notificationStatus = status;
-}
-```
-
-### 2. Enable Notification
-
-```typescript
-async enableNativeNotification() {
- try {
- const platformService = PlatformServiceFactory.getInstance();
-
- // 1. Request permissions if needed
- const permissions = await platformService.checkNotificationPermissions();
- if (permissions === null || permissions.notifications !== 'granted') {
- const result = await platformService.requestNotificationPermissions();
- if (result === null || !result.notifications) {
- throw new Error("Notification permissions denied");
- }
- }
-
- // 2. Schedule notification via PlatformService
- // Time is in device's local system time (from HTML5 time input)
- // PlatformService handles timezone and scheduling internally
- await platformService.scheduleDailyNotification({
- time: this.nativeNotificationTimeStorage, // "09:00" in local time
- title: this.nativeNotificationTitle,
- body: this.nativeNotificationMessage,
- sound: true,
- priority: 'high'
- });
-
- // 3. Save to settings
- await this.$saveSettings({
- nativeNotificationTime: this.nativeNotificationTimeStorage,
- nativeNotificationTitle: this.nativeNotificationTitle,
- nativeNotificationMessage: this.nativeNotificationMessage,
- });
-
- // 4. Update UI state
- this.nativeNotificationEnabled = true;
-
- this.notify.success("Daily notification scheduled successfully", TIMEOUTS.SHORT);
- } catch (error) {
- logger.error("Failed to enable notification:", error);
- this.notify.error("Failed to schedule notification. Please try again.", TIMEOUTS.LONG);
- }
-}
-
-### 3. Disable Notification
-
-```typescript
-async disableNativeNotification() {
- try {
- const platformService = PlatformServiceFactory.getInstance();
-
- // 1. Cancel notification via PlatformService
- await platformService.cancelDailyNotification();
-
- // 2. Clear settings
- await this.$saveSettings({
- nativeNotificationTime: "",
- nativeNotificationTitle: "",
- nativeNotificationMessage: "",
- });
-
- // 3. Update UI state
- this.nativeNotificationEnabled = false;
- this.nativeNotificationTime = "";
- this.nativeNotificationTimeStorage = "";
-
- this.notify.success("Daily notification disabled", TIMEOUTS.SHORT);
- } catch (error) {
- logger.error("Failed to disable native notification:", error);
- this.notify.error("Failed to disable notification. Please try again.", TIMEOUTS.LONG);
- }
-}
-```
-
-### 4. Edit Time
-
-**Approach**: Use inline HTML5 time input for quick edits
-
-```typescript
-async editNativeNotificationTime() {
- // Show inline HTML5 time input for quick changes
- // For complex editing (title, message), navigate to ScheduleView
- this.showTimeEdit = true;
-}
-```
-
-**Implementation Note**: HTML5 time input provides native mobile picker experience when shown inline, making it ideal for quick time adjustments in AccountViewView.
-
----
-
-## Settings Schema
-
-### New Settings Fields
-
-```typescript
-// Add to Settings interface in src/db/tables/settings.ts
-interface Settings {
- // ... existing fields ...
-
- // Native notification settings (Capacitor only)
- nativeNotificationTime?: string; // "09:00" format (24-hour)
- nativeNotificationTitle?: string; // Default: "Daily Update"
- nativeNotificationMessage?: string; // Default message
-}
-```
-
-### Settings Persistence
-
-- Store in `settings` table via `$saveSettings()`
-- Use same pattern as `notifyingNewActivityTime`
-- Persist across app restarts
-- Sync with plugin state on component mount
-
----
-
-## Plugin Integration
-
-### PlatformService Usage
-
-```typescript
-import { PlatformServiceFactory } from '@/services/PlatformServiceFactory';
-
-const platformService = PlatformServiceFactory.getInstance();
-
-// Check if notifications are supported
-const status = await platformService.getDailyNotificationStatus();
-if (status === null) {
- // Notifications not supported on this platform
-}
-```
-
-### Key Operations (via PlatformService)
-
-1. **Check Status**: `getDailyNotificationStatus()` - Returns status or `null` if unsupported
-2. **Check Permissions**: `checkNotificationPermissions()` - Returns permissions or `null` if unsupported
-3. **Request Permissions**: `requestNotificationPermissions()` - Requests permissions or returns `null` if unsupported
-4. **Schedule**: `scheduleDailyNotification(options)` - Schedules notification or throws error if unsupported
-5. **Cancel**: `cancelDailyNotification()` - Cancels notification or throws error if unsupported
-
----
-
-## UI/UX Considerations
-
-### Visual Design
-- **Section Style**: Match existing notification section (`bg-slate-100 rounded-md`)
-- **Toggle Switch**: Reuse existing custom toggle pattern
-- **Time Display**: Show in user-friendly format ("9:00 AM")
-- **Edit Button**: Small, subtle link/button to edit time
-
-### User Feedback
-- **Success**: Toast notification when scheduled successfully
-- **Error**: Clear error message with troubleshooting guidance
-- **Loading**: Show loading state during plugin operations
-- **Permission Request**: Handle gracefully if denied
-
-### Accessibility
-- **ARIA Labels**: Proper labels for all interactive elements
-- **Keyboard Navigation**: Full keyboard support
-- **Screen Reader**: Clear announcements for state changes
-
----
-
-## Implementation Phases
-
-### Phase 1: Basic Integration
-- [ ] Add platform detection property
-- [ ] Create native notification section in template
-- [ ] Add component state properties
-- [ ] Implement toggle functionality
-- [ ] Basic enable/disable operations
-
-### Phase 2: Time Management
-- [ ] Add time input (HTML5 time picker)
-- [ ] Implement time format conversion
-- [ ] Add edit time functionality
-- [ ] Save/load time from settings
-
-### Phase 3: Plugin Integration
-- [ ] Integrate with DailyNotificationFactory
-- [ ] Schedule notification on enable
-- [ ] Cancel notification on disable
-- [ ] Update notification on time change
-- [ ] Sync plugin state with settings
-
-### Phase 4: Polish & Error Handling
-- [ ] Permission request flow
-- [ ] Error handling and user feedback
-- [ ] Status verification
-- [ ] Help/info dialogs
-- [ ] Accessibility improvements
-
----
-
-## Implementation Decisions
-
-### Time Input Format ✅
-- **Selected**: HTML5 `` for Capacitor platforms
-- **Rationale**: Native mobile experience, simpler code, automatic 24-hour format
-
-### Edit Approach ✅
-- **Selected**: Inline HTML5 time input for quick edits in AccountViewView
-- **Note**: For complex editing (title, message changes), users can navigate to dedicated ScheduleView
-
-### Settings Field Names ✅
-- **Selected**: `nativeNotificationTime`, `nativeNotificationTitle`, `nativeNotificationMessage`
-- **Rationale**: Clear distinction from web push notification fields
-
-### Notification Title/Message ✅
-- **Selected**: Allow customization, default to "Daily Update" / "Your daily notification is ready!"
-- **Rationale**: Flexibility for users, sensible defaults
-
----
-
-## Success Criteria
-
-- [ ] Native notification section appears only on Capacitor platforms
-- [ ] Toggle enables/disables notifications via plugin
-- [ ] Time can be set and edited
-- [ ] Settings persist across app restarts
-- [ ] Plugin state syncs with settings
-- [ ] Error handling provides clear user feedback
-- [ ] UI matches existing design patterns
-- [ ] Accessibility requirements met
-
----
-
-## Related Components
-
-- **PlatformService**: Interface for platform capabilities (notification methods)
-- **PlatformServiceFactory**: Factory for getting platform service instance
-- **ScheduleView**: Dedicated scheduling interface (for complex editing)
-- **AccountViewView**: Main settings view (integration target)
-
----
-
-## Next Steps
-
-1. ~~**Decide on Approach**: Separate native notification section~~ ✅ **DECIDED**
-2. **Define Settings Schema**: Add native notification fields to Settings interface
-3. **Create UI Components**: Build notification section in AccountViewView
-4. **Integrate Plugin**: Connect UI to DailyNotificationFactory service
-5. **Test Flow**: Verify enable/disable/edit workflows
-6. **Add Help Content**: Create help documentation for native notifications
-
----
-
-**See also**:
-- `doc/daily-notification-plugin-integration-plan.md` - Overall integration plan
-- `src/views/AccountViewView.vue` - Target component for integration
-- `src/services/PlatformService.ts` - PlatformService interface definition
-- `src/services/PlatformServiceFactory.ts` - Factory for platform service instances
-
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index ba07104a..992e4485 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -16,7 +16,7 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
- **Platform**: All platforms (Capacitor provides full functionality, Web/Electron return null)
- **Architecture**: PlatformService interface integration (all platforms implement, unsupported return null)
-- **Components**: Home view (diagnostics/status) + Schedule view (time setting)
+- **Components**: Home view (diagnostics/status), Schedule view (time setting), AccountViewView integration (settings UI)
- **Store**: Pinia store for notification state management
- **Routes**: New routes for schedule, notifications, history, settings views
@@ -30,6 +30,7 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
- **Medium**: New Vue components, Pinia store, router routes
- **Pattern**: Following PlatformService interface pattern (like camera, filesystem methods) - all platforms implement, unsupported return null
- **Integration**: Plugin API integration with error handling
+- **UI Integration**: AccountViewView modification with new notification section
#### Platform Impact
- **Single Platform**: Capacitor-only (Android/iOS)
@@ -43,6 +44,7 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
- Notification scheduling
- Status checking
- Cross-platform validation (ensure web/electron unaffected)
+ - AccountViewView UI integration
### Dependency Complexity
@@ -52,10 +54,13 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
- Store creation (Pinia)
- Component dependencies (ActionCard, StatusCard)
- Logger integration (replace console.* with project logger)
+ - AccountViewView modifications
+ - Settings schema updates
#### External Dependencies
- **Medium**:
- `@timesafari/daily-notification-plugin` (external package)
+ - `@capacitor/core` (already in project)
- Capacitor core APIs
- Platform detection utilities
@@ -79,6 +84,9 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
4. **Store State Management**: Notification state persistence
- **Mitigation**: Follow existing Pinia patterns in codebase
+5. **AccountViewView Integration**: UI changes must not affect existing functionality
+ - **Mitigation**: Use platform capability detection, hide UI on unsupported platforms
+
---
## Platform Analysis
@@ -250,7 +258,8 @@ src/views/
├── ScheduleView.vue (new - notification scheduling)
├── NotificationsView.vue (new - view scheduled notifications)
├── NotificationHistoryView.vue (new - notification history)
- └── NotificationSettingsView.vue (new - notification settings)
+ ├── NotificationSettingsView.vue (new - notification settings)
+ └── AccountViewView.vue (existing - add Daily Notifications section)
```
#### Supporting Components
@@ -297,6 +306,341 @@ src/stores/
---
+## AccountViewView Integration Strategy
+
+### Overview
+
+Integrate daily notification scheduling into `AccountViewView.vue`, allowing users to configure notification times directly from their account settings.
+
+### Integration Approach ✅ **ACCEPTED**
+
+**Decision**: Create a separate "Daily Notifications" section
+
+This approach adds a dedicated "Daily Notifications" section that checks PlatformService capabilities. On Capacitor platforms, it provides full functionality. On other platforms, the UI is hidden when PlatformService returns `null` for notification methods.
+
+**Key Benefits**:
+- Uses PlatformService interface pattern (consistent with camera, filesystem)
+- Platform-specific features properly isolated
+- Can use native time picker (better UX on mobile)
+- Future-proof: Easy to extend with additional notification features
+- Graceful degradation on unsupported platforms
+
+### UI Component Design
+
+#### 1. Platform Capability Detection
+
+```typescript
+// In AccountViewView component
+async checkNotificationSupport(): Promise {
+ const platformService = PlatformServiceFactory.getInstance();
+ const status = await platformService.getDailyNotificationStatus();
+ return status !== null; // null means not supported
+}
+```
+
+#### 2. State Management
+
+```typescript
+// Component properties
+nativeNotificationEnabled: boolean = false;
+nativeNotificationTime: string = ""; // Display format: "9:00 AM"
+nativeNotificationTimeStorage: string = ""; // Plugin format: "09:00"
+nativeNotificationTitle: string = "Daily Update";
+nativeNotificationMessage: string = "Your daily notification is ready!";
+notificationsSupported: boolean = false; // Computed from PlatformService
+```
+
+#### 3. Template Section
+
+```vue
+
+
+
+ Daily Notifications
+
+
+
+
+
Daily Notification
+
+
+
+
+
+
+
+
+
+ Scheduled for: {{ nativeNotificationTime }}
+
+
+
+
+```
+
+#### 4. Time Input ✅ **SELECTED: HTML5 Time Input**
+
+**Decision**: Use HTML5 `` for native mobile experience
+
+```vue
+
+```
+
+**Benefits**:
+- Native mobile time picker UI on Capacitor platforms
+- Simpler implementation (no custom time parsing needed)
+- Automatic 24-hour format output (compatible with plugin)
+- System handles locale-specific time formatting
+- Better UX on mobile devices
+
+**Note**: HTML5 time input provides time in "HH:mm" format (24-hour) which matches the plugin's expected format perfectly.
+
+#### 5. Time Format Conversion (Using System Time)
+
+**Key Principle**: Use device's local system time - no timezone conversions needed. The plugin handles system time natively.
+
+```typescript
+// Convert "09:00" (plugin storage format) to "9:00 AM" (display)
+function formatTimeForDisplay(time24: string): string {
+ const [hours, minutes] = time24.split(':');
+ const hourNum = parseInt(hours);
+ const isPM = hourNum >= 12;
+ const displayHour = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
+ return `${displayHour}:${minutes} ${isPM ? 'PM' : 'AM'}`;
+}
+
+// HTML5 time input provides "HH:mm" in local time - use directly
+// No UTC conversion needed - plugin handles local timezone
+function getTimeFromInput(timeInput: string): string {
+ // timeInput is already in "HH:mm" format from
+ // This is in the user's local timezone - pass directly to plugin
+ return timeInput; // e.g., "09:00" in user's local time
+}
+```
+
+**Time Handling**:
+- **PlatformService Integration**: Uses device's local system time directly - NO UTC conversion needed. The plugin schedules notifications on the device itself, using the device's timezone.
+
+**Implementation Principles**:
+- HTML5 `` provides time in device's local timezone
+- Plugin receives time in "HH:mm" format and schedules relative to device's local time
+- No manual timezone conversion or UTC calculations needed
+- System automatically handles:
+ - Timezone changes
+ - Daylight saving time transitions
+ - Device timezone updates
+- User sets "9:00 AM" in their local time → plugin schedules for 9:00 AM local time every day
+
+### Data Flow
+
+#### 1. Initialization
+
+```typescript
+async initializeState() {
+ // ... existing initialization ...
+
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // Check if notifications are supported on this platform
+ const status = await platformService.getDailyNotificationStatus();
+ if (status === null) {
+ // Notifications not supported - don't initialize
+ this.notificationsSupported = false;
+ return;
+ }
+
+ this.notificationsSupported = true;
+
+ // Load from settings
+ const nativeNotificationTime = settings.nativeNotificationTime || "";
+ this.nativeNotificationEnabled = !!nativeNotificationTime;
+ this.nativeNotificationTimeStorage = nativeNotificationTime;
+
+ if (nativeNotificationTime) {
+ this.nativeNotificationTime = formatTimeForDisplay(nativeNotificationTime);
+ }
+
+ // Update UI with current status
+ this.notificationStatus = status;
+}
+```
+
+#### 2. Enable Notification
+
+```typescript
+async enableNativeNotification() {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // 1. Request permissions if needed
+ const permissions = await platformService.checkNotificationPermissions();
+ if (permissions === null || permissions.notifications !== 'granted') {
+ const result = await platformService.requestNotificationPermissions();
+ if (result === null || !result.notifications) {
+ throw new Error("Notification permissions denied");
+ }
+ }
+
+ // 2. Schedule notification via PlatformService
+ // Time is in device's local system time (from HTML5 time input)
+ // PlatformService handles timezone and scheduling internally
+ await platformService.scheduleDailyNotification({
+ time: this.nativeNotificationTimeStorage, // "09:00" in local time
+ title: this.nativeNotificationTitle,
+ body: this.nativeNotificationMessage,
+ sound: true,
+ priority: 'high'
+ });
+
+ // 3. Save to settings
+ await this.$saveSettings({
+ nativeNotificationTime: this.nativeNotificationTimeStorage,
+ nativeNotificationTitle: this.nativeNotificationTitle,
+ nativeNotificationMessage: this.nativeNotificationMessage,
+ });
+
+ // 4. Update UI state
+ this.nativeNotificationEnabled = true;
+
+ this.notify.success("Daily notification scheduled successfully", TIMEOUTS.SHORT);
+ } catch (error) {
+ logger.error("Failed to enable notification:", error);
+ this.notify.error("Failed to schedule notification. Please try again.", TIMEOUTS.LONG);
+ }
+}
+```
+
+#### 3. Disable Notification
+
+```typescript
+async disableNativeNotification() {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // 1. Cancel notification via PlatformService
+ await platformService.cancelDailyNotification();
+
+ // 2. Clear settings
+ await this.$saveSettings({
+ nativeNotificationTime: "",
+ nativeNotificationTitle: "",
+ nativeNotificationMessage: "",
+ });
+
+ // 3. Update UI state
+ this.nativeNotificationEnabled = false;
+ this.nativeNotificationTime = "";
+ this.nativeNotificationTimeStorage = "";
+
+ this.notify.success("Daily notification disabled", TIMEOUTS.SHORT);
+ } catch (error) {
+ logger.error("Failed to disable native notification:", error);
+ this.notify.error("Failed to disable notification. Please try again.", TIMEOUTS.LONG);
+ }
+}
+```
+
+#### 4. Edit Time
+
+**Approach**: Use inline HTML5 time input for quick edits
+
+```typescript
+async editNativeNotificationTime() {
+ // Show inline HTML5 time input for quick changes
+ // For complex editing (title, message), navigate to ScheduleView
+ this.showTimeEdit = true;
+}
+```
+
+**Implementation Note**: HTML5 time input provides native mobile picker experience when shown inline, making it ideal for quick time adjustments in AccountViewView.
+
+### Settings Schema
+
+#### New Settings Fields
+
+```typescript
+// Add to Settings interface in src/db/tables/settings.ts
+interface Settings {
+ // ... existing fields ...
+
+ // Native notification settings (Capacitor only)
+ nativeNotificationTime?: string; // "09:00" format (24-hour)
+ nativeNotificationTitle?: string; // Default: "Daily Update"
+ nativeNotificationMessage?: string; // Default message
+}
+```
+
+#### Settings Persistence
+
+- Store in `settings` table via `$saveSettings()`
+- Use same pattern as `notifyingNewActivityTime`
+- Persist across app restarts
+- Sync with plugin state on component mount
+
+### UI/UX Considerations
+
+#### Visual Design
+- **Section Style**: Match existing notification section (`bg-slate-100 rounded-md`)
+- **Toggle Switch**: Reuse existing custom toggle pattern
+- **Time Display**: Show in user-friendly format ("9:00 AM")
+- **Edit Button**: Small, subtle link/button to edit time
+
+#### User Feedback
+- **Success**: Toast notification when scheduled successfully
+- **Error**: Clear error message with troubleshooting guidance
+- **Loading**: Show loading state during plugin operations
+- **Permission Request**: Handle gracefully if denied
+
+#### Accessibility
+- **ARIA Labels**: Proper labels for all interactive elements
+- **Keyboard Navigation**: Full keyboard support
+- **Screen Reader**: Clear announcements for state changes
+
+### Implementation Decisions ✅
+
+#### Time Input Format ✅
+- **Selected**: HTML5 `` for Capacitor platforms
+- **Rationale**: Native mobile experience, simpler code, automatic 24-hour format
+
+#### Edit Approach ✅
+- **Selected**: Inline HTML5 time input for quick edits in AccountViewView
+- **Note**: For complex editing (title, message changes), users can navigate to dedicated ScheduleView
+
+#### Settings Field Names ✅
+- **Selected**: `nativeNotificationTime`, `nativeNotificationTitle`, `nativeNotificationMessage`
+- **Rationale**: Clear distinction from web push notification fields
+
+#### Notification Title/Message ✅
+- **Selected**: Allow customization, default to "Daily Update" / "Your daily notification is ready!"
+- **Rationale**: Flexibility for users, sensible defaults
+
+---
+
## Phase Breakdown
### Phase 1: Foundation & Infrastructure
@@ -344,10 +688,10 @@ src/stores/
---
-### Phase 2: Core Components
+### Phase 2: Core Components & AccountViewView Integration
**Complexity**: Medium
-**Goals**: Create reusable components and main views
+**Goals**: Create reusable components, main views, and AccountViewView integration
#### Tasks
1. **Reusable Components**
@@ -370,24 +714,33 @@ src/stores/
- Replace `console.*` with project logger
- Add loading states
-4. **AccountViewView Integration** ✅ **ACCEPTED: Option A**
+4. **AccountViewView Integration** ✅ **ACCEPTED**
- Add separate "Daily Notifications" section
- - Check platform capabilities before showing UI
+ - Check platform capabilities before showing UI (`v-if="notificationsSupported"`)
+ - Add computed property for platform capability detection
- Add toggle switch for enabling/disabling notifications
- Add HTML5 time input for scheduling time
- Integrate with PlatformService via PlatformServiceFactory
- Save/load settings from `settings` table
+ - Implement time format conversion (display vs storage)
+ - Add enable/disable notification methods
+ - Add edit time functionality
+ - Add permission request flow
+ - Add error handling and user feedback
#### Acceptance Criteria
- [ ] ActionCard and StatusCard components created
- [ ] Home view shows notification diagnostics
- [ ] Schedule view allows notification scheduling
-- [ ] AccountViewView has separate "Daily Notifications" section (Option A accepted)
+- [ ] AccountViewView has separate "Daily Notifications" section
- [ ] Notification section checks PlatformService capabilities before showing
- [ ] Toggle and time input functional in AccountViewView
+- [ ] Settings persist across app restarts
+- [ ] Plugin state syncs with settings
- [ ] All logging uses project logger
- [ ] Error handling implemented
- [ ] Loading states visible
+- [ ] UI matches existing design patterns
---
@@ -436,11 +789,13 @@ src/stores/
- Test permission requests
- Test status updates
- Test native fetcher configuration
+ - Test AccountViewView integration on Capacitor
2. **Cross-Platform Validation**
- Verify web build doesn't break
- Verify Electron build doesn't break
- Verify feature is hidden on non-Capacitor platforms
+ - Verify AccountViewView section hidden on web/electron
- Test graceful degradation
3. **Integration Testing**
@@ -448,6 +803,7 @@ src/stores/
- Test status checking workflow
- Test navigation between views
- Test store state persistence
+ - Test AccountViewView enable/disable/edit workflows
4. **Error Scenarios**
- Test plugin unavailable scenarios
@@ -458,6 +814,7 @@ src/stores/
#### Acceptance Criteria
- [ ] All Capacitor tests passing
- [ ] Web/Electron builds unaffected
+- [ ] AccountViewView integration verified on all platforms
- [ ] Integration tests passing
- [ ] Error scenarios handled gracefully
- [ ] Documentation updated
@@ -468,7 +825,7 @@ src/stores/
### Milestone 1: Foundation Complete
**Success Criteria**:
-- Factory service operational
+- PlatformService interface extended
- Store created and tested
- Routes accessible
- No build regressions
@@ -477,6 +834,7 @@ src/stores/
**Success Criteria**:
- Home view shows diagnostics
- Schedule view functional
+- AccountViewView integration complete
- Plugin integration working
- Logging standardized
@@ -502,24 +860,27 @@ src/stores/
- Factory service platform detection
- Store actions and state management
- Component rendering and interactions
+- AccountViewView notification section rendering
### Integration Tests
- Plugin API calls
- Permission flows
- Status updates
- Navigation between views
+- AccountViewView enable/disable/edit workflows
### Platform Tests
-- **Capacitor Android**: Notification scheduling, permissions, status
-- **Capacitor iOS**: Notification scheduling, permissions, status
-- **Web**: Feature hidden, no errors
-- **Electron**: Feature hidden, no errors
+- **Capacitor Android**: Notification scheduling, permissions, status, AccountViewView UI
+- **Capacitor iOS**: Notification scheduling, permissions, status, AccountViewView UI
+- **Web**: Feature hidden, no errors, AccountViewView section hidden
+- **Electron**: Feature hidden, no errors, AccountViewView section hidden
### E2E Tests (Playwright)
- Full notification scheduling workflow
- Permission request flow
- Status checking workflow
- Error handling scenarios
+- AccountViewView notification configuration workflow
---
@@ -537,6 +898,8 @@ src/stores/
- Platform detection utilities
- Router configuration
- Existing component patterns
+- AccountViewView component
+- Settings schema and persistence
### Configuration Dependencies
- **Active DID Management**: Use `$getActiveIdentity()` from `PlatformServiceMixin` (existing)
@@ -561,6 +924,7 @@ src/stores/
- **Service Interface**: Abstract interface with platform implementations
- **Store Pattern**: Pinia store for state management
- **Composition API vs Class**: Use provided code style (Composition API for HomeView, Class for ScheduleView)
+- **PlatformService Pattern**: Check capabilities via method results, not environment variables
### PlatformService Integration Strategy
@@ -615,6 +979,7 @@ await platformService.scheduleDailyNotification({
- Use dynamic imports exclusively
- Test web/electron builds after each phase
- Ensure no static plugin imports
+- Verify AccountViewView section properly hidden
### Risk 4: Configuration Dependencies (RESOLVED)
**Mitigation**:
@@ -623,14 +988,22 @@ await platformService.scheduleDailyNotification({
- **Use existing settings**: Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
- **No config files needed**: The HomeView component code references `TEST_USER_ZERO_CONFIG`, but should instead use the currently active DID and settings
+### Risk 5: AccountViewView Integration Issues
+**Mitigation**:
+- Use platform capability detection before showing UI
+- Test on all platforms to ensure proper hiding
+- Follow existing UI patterns for consistency
+- Add comprehensive error handling
+
---
## Success Criteria Summary
-- [ ] Plugin integrated using factory architecture
+- [ ] Plugin integrated using PlatformService architecture
- [ ] Feature works on Capacitor (Android/iOS)
- [ ] Feature hidden/graceful on Web/Electron
- [ ] All components created and functional
+- [ ] AccountViewView integration complete and functional
- [ ] Store manages notification state
- [ ] Router routes accessible
- [ ] Logging standardized (no console.*)
@@ -648,6 +1021,7 @@ await platformService.scheduleDailyNotification({
3. **Extend PlatformService**: Add notification methods to PlatformService interface and implement in all platform services
4. **Set Up Store**: Create Pinia store for notification state
5. **Begin Phase 1 Implementation**: Start with foundation tasks
+6. **AccountViewView Integration**: Implement Daily Notifications section in Phase 2
---
@@ -656,3 +1030,4 @@ await platformService.scheduleDailyNotification({
- `.cursor/rules/app/architectural_patterns.mdc` - Architecture patterns
- `.cursor/rules/app/timesafari_platforms.mdc` - Platform requirements
- `src/services/QRScanner/QRScannerFactory.ts` - Factory pattern reference
+- `src/views/AccountViewView.vue` - Target component for integration
--
2.30.2
From 7ae3b241dd88c1a2ea7398ad3840a180496b5065 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 04:04:18 +0000
Subject: [PATCH 05/17] docs: add explicit requirement for components to hide
on unsupported platforms
- Add CRITICAL REQUIREMENT in Platform Detection Strategy section
- Add Component Visibility Requirements listing all components
- Add Component Visibility Requirements section in Implementation Notes
- Include required pattern with code examples for component hiding
- Add verification checklist for component hiding
- Update Phase 2 tasks to require platform support checks
- Update Phase 3 tasks to require hiding for all notification views
- Add Risk 6 for components visible on unsupported platforms
- Update acceptance criteria to verify component hiding
- Update success criteria to verify hiding on Web/Electron platforms
---
...ly-notification-plugin-integration-plan.md | 90 +++++++++++++++++++
1 file changed, 90 insertions(+)
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index 992e4485..154c8573 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -107,6 +107,8 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
**Pattern**: PlatformService interface - all platforms implement methods
+**CRITICAL REQUIREMENT**: All notification scheduling components MUST hide themselves if the current device does not support scheduling.
+
Components check PlatformService capabilities by calling methods and checking for `null` returns:
```typescript
@@ -127,6 +129,12 @@ if (status === null) {
- Unsupported platforms return `null` or throw clear errors
- Components handle capability detection via method results, not environment variables
+**Component Visibility Requirements**:
+- **ScheduleView**: Must check platform support and hide/render placeholder if unsupported
+- **AccountViewView notification section**: Must use `v-if="notificationsSupported"` to hide section
+- **NotificationSettingsView**: Must check platform support before rendering
+- **Any component providing scheduling UI**: Must verify `getDailyNotificationStatus() !== null` before showing scheduling controls
+
### Web/Electron Implementation Strategy
#### Web Platform
@@ -709,6 +717,8 @@ interface Settings {
3. **Schedule View**
- Create `ScheduleView.vue` (provided code as reference)
+ - **REQUIRED**: Check platform support on mount - hide component if `getDailyNotificationStatus()` returns `null`
+ - **REQUIRED**: Render placeholder/unsupported message if device doesn't support scheduling
- Integrate with PlatformService via PlatformServiceFactory
- Add error handling
- Replace `console.*` with project logger
@@ -732,7 +742,9 @@ interface Settings {
- [ ] ActionCard and StatusCard components created
- [ ] Home view shows notification diagnostics
- [ ] Schedule view allows notification scheduling
+- [ ] **ScheduleView hides itself on unsupported platforms** (returns null check)
- [ ] AccountViewView has separate "Daily Notifications" section
+- [ ] **AccountViewView notification section hidden on unsupported platforms** (`v-if="notificationsSupported"`)
- [ ] Notification section checks PlatformService capabilities before showing
- [ ] Toggle and time input functional in AccountViewView
- [ ] Settings persist across app restarts
@@ -752,8 +764,11 @@ interface Settings {
#### Tasks
1. **Supporting Views**
- Create `NotificationsView.vue` (list scheduled notifications)
+ - **REQUIRED**: Hide component if `getDailyNotificationStatus()` returns `null`
- Create `NotificationHistoryView.vue` (notification history)
+ - **REQUIRED**: Hide component if `getDailyNotificationStatus()` returns `null`
- Create `NotificationSettingsView.vue` (settings/preferences)
+ - **REQUIRED**: Hide component if `getDailyNotificationStatus()` returns `null`
2. **Native Fetcher Configuration**
- Integrate `configureNativeFetcher()` in HomeView
@@ -769,6 +784,7 @@ interface Settings {
#### Acceptance Criteria
- [ ] All supporting views created and functional
+- [ ] **All notification views hide themselves on unsupported platforms**
- [ ] Native fetcher configuration working
- [ ] Permission requests handled properly
- [ ] Status updates after permission changes
@@ -911,6 +927,70 @@ interface Settings {
## Implementation Notes
+### Component Visibility Requirements
+
+**CRITICAL**: All components that provide notification scheduling UI MUST hide themselves if the current device does not support scheduling.
+
+#### Required Pattern for All Scheduling Components
+
+```typescript
+// In component mounted/created lifecycle
+async mounted() {
+ const platformService = PlatformServiceFactory.getInstance();
+ const status = await platformService.getDailyNotificationStatus();
+
+ if (status === null) {
+ // Device does not support scheduling - hide component
+ this.notificationsSupported = false;
+ return;
+ }
+
+ // Device supports scheduling - proceed with initialization
+ this.notificationsSupported = true;
+ // ... rest of initialization
+}
+```
+
+#### Template Pattern
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
Notifications are not supported on this platform.
+
+
+```
+
+#### Components That Must Implement This Pattern
+
+1. **ScheduleView.vue**: Check support on mount, hide if unsupported
+2. **AccountViewView.vue**: Daily Notifications section uses `v-if="notificationsSupported"`
+3. **NotificationSettingsView.vue**: Check support, hide if unsupported
+4. **NotificationsView.vue**: Check support, hide if unsupported
+5. **NotificationHistoryView.vue**: Check support, hide if unsupported
+6. **Any other component providing scheduling UI**: Must check and hide if unsupported
+
+#### Verification Checklist
+
+- [ ] ScheduleView checks platform support and hides on unsupported platforms
+- [ ] AccountViewView notification section hidden via `v-if` on unsupported platforms
+- [ ] NotificationSettingsView checks and hides on unsupported platforms
+- [ ] NotificationsView checks and hides on unsupported platforms
+- [ ] NotificationHistoryView checks and hides on unsupported platforms
+- [ ] All components tested on Web/Electron to verify hiding works
+- [ ] No console errors when components are hidden
+- [ ] Routes remain accessible but components show unsupported message (for ScheduleView)
+
### Code Quality Standards
- **Logging**: Use `logger` from `@/utils/logger`, not `console.*`
- **File Documentation**: Add file-level documentation headers
@@ -995,6 +1075,14 @@ await platformService.scheduleDailyNotification({
- Follow existing UI patterns for consistency
- Add comprehensive error handling
+### Risk 6: Components Visible on Unsupported Platforms
+**Mitigation**:
+- **REQUIRED**: All scheduling components must check `getDailyNotificationStatus()` and hide if `null`
+- Use `v-if="notificationsSupported"` pattern consistently
+- Add explicit verification in acceptance criteria
+- Test on Web/Electron builds to verify hiding works
+- Document required pattern in Implementation Notes section
+
---
## Success Criteria Summary
@@ -1003,12 +1091,14 @@ await platformService.scheduleDailyNotification({
- [ ] Feature works on Capacitor (Android/iOS)
- [ ] Feature hidden/graceful on Web/Electron
- [ ] All components created and functional
+- [ ] **All scheduling components hide themselves on unsupported platforms**
- [ ] AccountViewView integration complete and functional
- [ ] Store manages notification state
- [ ] Router routes accessible
- [ ] Logging standardized (no console.*)
- [ ] Error handling robust
- [ ] Cross-platform testing complete
+- [ ] **Verified component hiding on Web/Electron platforms**
- [ ] Documentation updated
- [ ] No build regressions
--
2.30.2
From 95ac1afcd26ef73f775ae6f49641f86e53e43956 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 04:41:27 +0000
Subject: [PATCH 06/17] docs: remove independent views, focus on
AccountViewView integration only
- Remove ScheduleView, NotificationsView, NotificationHistoryView, NotificationSettingsView
- Remove router routes for independent views
- Remove Pinia store (not needed - state managed locally)
- Remove HomeView diagnostics integration
- Remove native fetcher configuration integration
- Keep only AccountViewView integration with optional supporting components
- Update all phases to focus on AccountViewView only
- Update milestones and testing strategy
- Update dependencies to remove router/pinia references
- Clarify supporting components are optional and only if AccountViewView exceeds length limits
---
...ly-notification-plugin-integration-plan.md | 303 +++++-------------
1 file changed, 74 insertions(+), 229 deletions(-)
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index 154c8573..8b317a86 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -16,9 +16,8 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
- **Platform**: All platforms (Capacitor provides full functionality, Web/Electron return null)
- **Architecture**: PlatformService interface integration (all platforms implement, unsupported return null)
-- **Components**: Home view (diagnostics/status), Schedule view (time setting), AccountViewView integration (settings UI)
-- **Store**: Pinia store for notification state management
-- **Routes**: New routes for schedule, notifications, history, settings views
+- **Components**: AccountViewView integration (settings UI) with optional supporting components
+- **Store**: No store needed - state managed locally in AccountViewView
---
@@ -27,10 +26,10 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
### Technical Complexity: **Medium**
#### Code Changes
-- **Medium**: New Vue components, Pinia store, router routes
+- **Medium**: AccountViewView modification, optional supporting components
- **Pattern**: Following PlatformService interface pattern (like camera, filesystem methods) - all platforms implement, unsupported return null
- **Integration**: Plugin API integration with error handling
-- **UI Integration**: AccountViewView modification with new notification section
+- **UI Integration**: AccountViewView modification with new notification section and optional supporting components
#### Platform Impact
- **Single Platform**: Capacitor-only (Android/iOS)
@@ -50,9 +49,7 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
#### Internal Dependencies
- **Medium**:
- - Router configuration (new routes)
- - Store creation (Pinia)
- - Component dependencies (ActionCard, StatusCard)
+ - Optional supporting components (only if AccountViewView exceeds length limits)
- Logger integration (replace console.* with project logger)
- AccountViewView modifications
- Settings schema updates
@@ -82,10 +79,7 @@ This plan outlines the integration of `@timesafari/daily-notification-plugin` in
- **Mitigation**: Use dynamic imports with platform checks, graceful fallbacks
4. **Store State Management**: Notification state persistence
- - **Mitigation**: Follow existing Pinia patterns in codebase
-
-5. **AccountViewView Integration**: UI changes must not affect existing functionality
- - **Mitigation**: Use platform capability detection, hide UI on unsupported platforms
+ - **Mitigation**: State managed locally in AccountViewView - no store needed
---
@@ -130,9 +124,8 @@ if (status === null) {
- Components handle capability detection via method results, not environment variables
**Component Visibility Requirements**:
-- **ScheduleView**: Must check platform support and hide/render placeholder if unsupported
- **AccountViewView notification section**: Must use `v-if="notificationsSupported"` to hide section
-- **NotificationSettingsView**: Must check platform support before rendering
+- **Supporting components** (if created): Must check platform support before rendering
- **Any component providing scheduling UI**: Must verify `getDailyNotificationStatus() !== null` before showing scheduling controls
### Web/Electron Implementation Strategy
@@ -262,55 +255,18 @@ export interface NativeFetcherConfig {
#### Views Structure
```
src/views/
- ├── HomeView.vue (existing - modify to add notification diagnostics)
- ├── ScheduleView.vue (new - notification scheduling)
- ├── NotificationsView.vue (new - view scheduled notifications)
- ├── NotificationHistoryView.vue (new - notification history)
- ├── NotificationSettingsView.vue (new - notification settings)
└── AccountViewView.vue (existing - add Daily Notifications section)
```
-#### Supporting Components
-```
-src/components/cards/
- ├── ActionCard.vue (new - reusable action card)
- └── StatusCard.vue (new - reusable status card)
-```
-
-#### Store Structure
+#### Supporting Components (Optional - Only if AccountViewView needs extraction)
```
-src/stores/
- └── app.ts (new - Pinia store for app-wide state)
- - notificationStatus: NotificationStatus | null
- - platform: 'web' | 'capacitor' | 'electron'
- - setNotificationStatus(status): void
+src/components/notifications/ (optional)
+ ├── NotificationToggle.vue (optional - extract toggle if AccountViewView too long)
+ ├── NotificationTimePicker.vue (optional - extract time picker if needed)
+ └── NotificationStatusDisplay.vue (optional - extract status display if needed)
```
-### Router Integration
-
-```typescript
-// src/router/index.ts - Add new routes
-{
- path: "/schedule",
- name: "schedule",
- component: () => import("../views/ScheduleView.vue"),
-},
-{
- path: "/notifications",
- name: "notifications",
- component: () => import("../views/NotificationsView.vue"),
-},
-{
- path: "/history",
- name: "notification-history",
- component: () => import("../views/NotificationHistoryView.vue"),
-},
-{
- path: "/settings",
- name: "settings",
- component: () => import("../views/NotificationSettingsView.vue"),
-},
-```
+**Note**: Supporting components should only be created if AccountViewView exceeds reasonable length limits (>200 lines). Keep everything in AccountViewView if possible.
---
@@ -580,7 +536,6 @@ async disableNativeNotification() {
```typescript
async editNativeNotificationTime() {
// Show inline HTML5 time input for quick changes
- // For complex editing (title, message), navigate to ScheduleView
this.showTimeEdit = true;
}
```
@@ -637,7 +592,7 @@ interface Settings {
#### Edit Approach ✅
- **Selected**: Inline HTML5 time input for quick edits in AccountViewView
-- **Note**: For complex editing (title, message changes), users can navigate to dedicated ScheduleView
+- **Note**: All editing happens within AccountViewView - no separate views needed
#### Settings Field Names ✅
- **Selected**: `nativeNotificationTime`, `nativeNotificationTitle`, `nativeNotificationMessage`
@@ -669,62 +624,33 @@ interface Settings {
- Implement in `WebPlatformService` with null returns / error throws
- Implement in `ElectronPlatformService` with null returns / error throws
-3. **Pinia Store Setup**
- - Create `src/stores/app.ts` with notification state
- - Define `NotificationStatus` interface
- - Implement `setNotificationStatus()` action
- - Add platform detection to store
-
-4. **Native Fetcher Configuration Integration**
- - Update HomeView `configureNativeFetcher()` to use active DID management
- - Replace `TEST_USER_ZERO_CONFIG` references with `$getActiveIdentity()`
- - Replace `generateEndorserJWT` with `createEndorserJwtForDid()` from `src/libs/endorserServer.ts`
- - Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
-
-5. **Router Routes**
- - Add route definitions for schedule, notifications, history, settings
- - Test route navigation
+3. **Settings Schema Extension**
+ - Add notification settings fields to Settings interface
+ - Update settings persistence methods if needed
#### Acceptance Criteria
- [ ] PlatformService interface extended with notification methods
- [ ] CapacitorPlatformService implements notification methods using plugin
- [ ] WebPlatformService and ElectronPlatformService return null/throw errors appropriately
-- [ ] Pinia store created and tested
-- [ ] HomeView `configureNativeFetcher()` updated to use active DID (no TEST_USER_ZERO_CONFIG)
-- [ ] Routes added and accessible
+- [ ] Settings schema extended with notification fields
- [ ] No build errors in web/electron builds
---
-### Phase 2: Core Components & AccountViewView Integration
+### Phase 2: AccountViewView Integration
**Complexity**: Medium
-**Goals**: Create reusable components, main views, and AccountViewView integration
+**Goals**: Integrate notification scheduling into AccountViewView with optional supporting components
#### Tasks
-1. **Reusable Components**
- - Create `ActionCard.vue` component
- - Create `StatusCard.vue` component
+1. **Supporting Components (Optional)**
+ - Create supporting components only if AccountViewView exceeds length limits
+ - Consider: `NotificationToggle.vue`, `NotificationTimePicker.vue`, `NotificationStatusDisplay.vue`
- Follow project styling patterns
- Add TypeScript interfaces
+ - Keep components focused and reusable within AccountViewView context
-2. **Home View Integration**
- - Modify existing `HomeView.vue` OR create new notification home view
- - Integrate plugin diagnostics
- - Add system status display
- - Connect to Pinia store
- - Replace `console.*` with project logger
-
-3. **Schedule View**
- - Create `ScheduleView.vue` (provided code as reference)
- - **REQUIRED**: Check platform support on mount - hide component if `getDailyNotificationStatus()` returns `null`
- - **REQUIRED**: Render placeholder/unsupported message if device doesn't support scheduling
- - Integrate with PlatformService via PlatformServiceFactory
- - Add error handling
- - Replace `console.*` with project logger
- - Add loading states
-
-4. **AccountViewView Integration** ✅ **ACCEPTED**
+2. **AccountViewView Integration** ✅ **ACCEPTED**
- Add separate "Daily Notifications" section
- Check platform capabilities before showing UI (`v-if="notificationsSupported"`)
- Add computed property for platform capability detection
@@ -739,10 +665,7 @@ interface Settings {
- Add error handling and user feedback
#### Acceptance Criteria
-- [ ] ActionCard and StatusCard components created
-- [ ] Home view shows notification diagnostics
-- [ ] Schedule view allows notification scheduling
-- [ ] **ScheduleView hides itself on unsupported platforms** (returns null check)
+- [ ] Supporting components created only if AccountViewView exceeds length limits
- [ ] AccountViewView has separate "Daily Notifications" section
- [ ] **AccountViewView notification section hidden on unsupported platforms** (`v-if="notificationsSupported"`)
- [ ] Notification section checks PlatformService capabilities before showing
@@ -756,84 +679,40 @@ interface Settings {
---
-### Phase 3: Supporting Views & Configuration
+### Phase 3: Polish & Testing
**Complexity**: Medium
-**Goals**: Complete all views and native fetcher configuration
+**Goals**: Complete AccountViewView integration, error handling, and testing
#### Tasks
-1. **Supporting Views**
- - Create `NotificationsView.vue` (list scheduled notifications)
- - **REQUIRED**: Hide component if `getDailyNotificationStatus()` returns `null`
- - Create `NotificationHistoryView.vue` (notification history)
- - **REQUIRED**: Hide component if `getDailyNotificationStatus()` returns `null`
- - Create `NotificationSettingsView.vue` (settings/preferences)
- - **REQUIRED**: Hide component if `getDailyNotificationStatus()` returns `null`
-
-2. **Native Fetcher Configuration**
- - Integrate `configureNativeFetcher()` in HomeView
- - Use `$getActiveIdentity()` to get active DID (replace TEST_USER_ZERO_CONFIG)
- - Use `createEndorserJwtForDid()` for JWT generation
- - Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
- - Add error handling for configuration failures
-
-3. **Permission Management**
- - Implement permission request flow
+1. **Permission Management**
+ - Implement permission request flow in AccountViewView
- Handle permission denial gracefully
- Update status after permission changes
+ - Show appropriate user feedback
+
+2. **Error Handling & User Feedback**
+ - Add comprehensive error handling for all plugin operations
+ - Implement loading states during async operations
+ - Add success/error toast notifications
+ - Handle edge cases (permission denied, plugin unavailable, etc.)
+
+3. **Testing & Validation**
+ - Test AccountViewView integration on Capacitor platforms
+ - Verify component hiding on Web/Electron
+ - Test all user workflows (enable, disable, edit time)
+ - Verify settings persistence
#### Acceptance Criteria
-- [ ] All supporting views created and functional
-- [ ] **All notification views hide themselves on unsupported platforms**
-- [ ] Native fetcher configuration working
- [ ] Permission requests handled properly
- [ ] Status updates after permission changes
- [ ] Error handling for all failure cases
+- [ ] User feedback (toasts, loading states) implemented
+- [ ] AccountViewView tested on all platforms
+- [ ] Component hiding verified on unsupported platforms
---
-### Phase 4: Testing & Validation
-
-**Complexity**: Medium-High
-**Goals**: Comprehensive testing across platforms and scenarios
-
-#### Tasks
-1. **Capacitor Testing**
- - Test plugin availability detection
- - Test notification scheduling on Android
- - Test notification scheduling on iOS
- - Test permission requests
- - Test status updates
- - Test native fetcher configuration
- - Test AccountViewView integration on Capacitor
-
-2. **Cross-Platform Validation**
- - Verify web build doesn't break
- - Verify Electron build doesn't break
- - Verify feature is hidden on non-Capacitor platforms
- - Verify AccountViewView section hidden on web/electron
- - Test graceful degradation
-
-3. **Integration Testing**
- - Test full scheduling workflow
- - Test status checking workflow
- - Test navigation between views
- - Test store state persistence
- - Test AccountViewView enable/disable/edit workflows
-
-4. **Error Scenarios**
- - Test plugin unavailable scenarios
- - Test permission denied scenarios
- - Test network failures (for native fetcher)
- - Test invalid configuration scenarios
-
-#### Acceptance Criteria
-- [ ] All Capacitor tests passing
-- [ ] Web/Electron builds unaffected
-- [ ] AccountViewView integration verified on all platforms
-- [ ] Integration tests passing
-- [ ] Error scenarios handled gracefully
-- [ ] Documentation updated
---
@@ -842,30 +721,22 @@ interface Settings {
### Milestone 1: Foundation Complete
**Success Criteria**:
- PlatformService interface extended
-- Store created and tested
-- Routes accessible
+- Settings schema extended
- No build regressions
-### Milestone 2: Core Features Operational
+### Milestone 2: AccountViewView Integration Complete
**Success Criteria**:
-- Home view shows diagnostics
-- Schedule view functional
-- AccountViewView integration complete
+- AccountViewView notification section functional
- Plugin integration working
-- Logging standardized
+- Settings persistence working
+- Component hiding verified on unsupported platforms
-### Milestone 3: Full Feature Set
-**Success Criteria**:
-- All views created and functional
-- Native fetcher configured
-- Permissions managed properly
-- Status updates working
-
-### Milestone 4: Production Ready
+### Milestone 3: Production Ready
**Success Criteria**:
- All tests passing
- Cross-platform validation complete
- Error handling robust
+- User feedback implemented
- Documentation complete
---
@@ -873,17 +744,16 @@ interface Settings {
## Testing Strategy
### Unit Tests
-- Factory service platform detection
-- Store actions and state management
-- Component rendering and interactions
+- PlatformService platform detection
- AccountViewView notification section rendering
+- Supporting component rendering (if created)
### Integration Tests
-- Plugin API calls
+- Plugin API calls from AccountViewView
- Permission flows
- Status updates
-- Navigation between views
- AccountViewView enable/disable/edit workflows
+- Settings persistence
### Platform Tests
- **Capacitor Android**: Notification scheduling, permissions, status, AccountViewView UI
@@ -892,11 +762,11 @@ interface Settings {
- **Electron**: Feature hidden, no errors, AccountViewView section hidden
### E2E Tests (Playwright)
-- Full notification scheduling workflow
+- AccountViewView notification configuration workflow
- Permission request flow
-- Status checking workflow
+- Enable/disable notification workflow
+- Edit notification time workflow
- Error handling scenarios
-- AccountViewView notification configuration workflow
---
@@ -906,22 +776,16 @@ interface Settings {
- `@timesafari/daily-notification-plugin` (to be added)
- `@capacitor/core` (already in project)
- `vue` (already in project)
-- `vue-router` (already in project)
-- `pinia` (already in project)
### Internal Dependencies
- Logger service (`@/utils/logger`)
- Platform detection utilities
-- Router configuration
- Existing component patterns
- AccountViewView component
- Settings schema and persistence
### Configuration Dependencies
-- **Active DID Management**: Use `$getActiveIdentity()` from `PlatformServiceMixin` (existing)
-- **JWT Generation**: Use `createEndorserJwtForDid(activeDid, payload)` from `src/libs/endorserServer.ts` (existing)
-- **Settings Access**: Use `$accountSettings()` for `apiServer` and `starredPlanHandleIds` (existing)
-- **No new config files needed**: Replace `TEST_USER_ZERO_CONFIG` references with active DID and settings
+- **Settings Access**: Use `$accountSettings()` and `$saveSettings()` for persistence (existing)
---
@@ -973,23 +837,16 @@ async mounted() {
#### Components That Must Implement This Pattern
-1. **ScheduleView.vue**: Check support on mount, hide if unsupported
-2. **AccountViewView.vue**: Daily Notifications section uses `v-if="notificationsSupported"`
-3. **NotificationSettingsView.vue**: Check support, hide if unsupported
-4. **NotificationsView.vue**: Check support, hide if unsupported
-5. **NotificationHistoryView.vue**: Check support, hide if unsupported
-6. **Any other component providing scheduling UI**: Must check and hide if unsupported
+1. **AccountViewView.vue**: Daily Notifications section uses `v-if="notificationsSupported"`
+2. **Supporting components** (if created): Must check support before rendering any scheduling UI
+3. **Any component providing scheduling UI**: Must verify `getDailyNotificationStatus() !== null` before showing scheduling controls
#### Verification Checklist
-- [ ] ScheduleView checks platform support and hides on unsupported platforms
- [ ] AccountViewView notification section hidden via `v-if` on unsupported platforms
-- [ ] NotificationSettingsView checks and hides on unsupported platforms
-- [ ] NotificationsView checks and hides on unsupported platforms
-- [ ] NotificationHistoryView checks and hides on unsupported platforms
+- [ ] Supporting components (if created) check and hide on unsupported platforms
- [ ] All components tested on Web/Electron to verify hiding works
- [ ] No console errors when components are hidden
-- [ ] Routes remain accessible but components show unsupported message (for ScheduleView)
### Code Quality Standards
- **Logging**: Use `logger` from `@/utils/logger`, not `console.*`
@@ -1000,10 +857,8 @@ async mounted() {
- **Line Length**: Keep methods < 80 columns when possible
### Architecture Patterns to Follow
-- **Factory Pattern**: Like `QRScannerFactory` for conditional loading
- **Service Interface**: Abstract interface with platform implementations
-- **Store Pattern**: Pinia store for state management
-- **Composition API vs Class**: Use provided code style (Composition API for HomeView, Class for ScheduleView)
+- **Component Organization**: Keep AccountViewView concise - extract supporting components if needed to maintain < 200 lines
- **PlatformService Pattern**: Check capabilities via method results, not environment variables
### PlatformService Integration Strategy
@@ -1061,21 +916,14 @@ await platformService.scheduleDailyNotification({
- Ensure no static plugin imports
- Verify AccountViewView section properly hidden
-### Risk 4: Configuration Dependencies (RESOLVED)
-**Mitigation**:
-- **Use existing active DID management**: Use `$getActiveIdentity()` from `PlatformServiceMixin` to get currently selected DID
-- **Use existing JWT generation**: Use `createEndorserJwtForDid(activeDid, payload)` from `src/libs/endorserServer.ts`
-- **Use existing settings**: Get `apiServer` and `starredPlanHandleIds` from `$accountSettings()`
-- **No config files needed**: The HomeView component code references `TEST_USER_ZERO_CONFIG`, but should instead use the currently active DID and settings
-
-### Risk 5: AccountViewView Integration Issues
+### Risk 4: AccountViewView Integration Issues
**Mitigation**:
- Use platform capability detection before showing UI
- Test on all platforms to ensure proper hiding
- Follow existing UI patterns for consistency
- Add comprehensive error handling
-### Risk 6: Components Visible on Unsupported Platforms
+### Risk 5: Components Visible on Unsupported Platforms
**Mitigation**:
- **REQUIRED**: All scheduling components must check `getDailyNotificationStatus()` and hide if `null`
- Use `v-if="notificationsSupported"` pattern consistently
@@ -1090,11 +938,9 @@ await platformService.scheduleDailyNotification({
- [ ] Plugin integrated using PlatformService architecture
- [ ] Feature works on Capacitor (Android/iOS)
- [ ] Feature hidden/graceful on Web/Electron
-- [ ] All components created and functional
-- [ ] **All scheduling components hide themselves on unsupported platforms**
- [ ] AccountViewView integration complete and functional
-- [ ] Store manages notification state
-- [ ] Router routes accessible
+- [ ] **AccountViewView notification section hides itself on unsupported platforms**
+- [ ] Settings persist across app restarts
- [ ] Logging standardized (no console.*)
- [ ] Error handling robust
- [ ] Cross-platform testing complete
@@ -1107,11 +953,10 @@ await platformService.scheduleDailyNotification({
## Next Steps
1. **Verify Plugin Package**: Confirm `@timesafari/daily-notification-plugin` availability
-2. **Update HomeView Configuration**: Replace `TEST_USER_ZERO_CONFIG` references in HomeView with existing active DID management (`$getActiveIdentity()`, `createEndorserJwtForDid()`, `$accountSettings()`)
-3. **Extend PlatformService**: Add notification methods to PlatformService interface and implement in all platform services
-4. **Set Up Store**: Create Pinia store for notification state
-5. **Begin Phase 1 Implementation**: Start with foundation tasks
-6. **AccountViewView Integration**: Implement Daily Notifications section in Phase 2
+2. **Extend PlatformService**: Add notification methods to PlatformService interface and implement in all platform services
+3. **Extend Settings Schema**: Add notification fields to Settings interface
+4. **Begin Phase 1 Implementation**: Start with foundation tasks
+5. **AccountViewView Integration**: Implement Daily Notifications section in Phase 2
---
--
2.30.2
From ae5f1a33a79a790bb65d275d97fd56b48ac9fa02 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 06:02:34 +0000
Subject: [PATCH 07/17] docs: add checkboxes to all actionable items in
integration plan
- Add checkboxes to Phase 1, 2, 3 task sub-items
- Add checkboxes to Milestone success criteria
- Add checkboxes to Testing Strategy test items
- Add checkboxes to Risk Mitigation mitigation items
- Add checkboxes to Next Steps
- All actionable items now have checkboxes for tracking progress
---
...ly-notification-plugin-integration-plan.md | 184 +++++++++---------
1 file changed, 92 insertions(+), 92 deletions(-)
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index 8b317a86..58606380 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -613,20 +613,20 @@ interface Settings {
#### Tasks
1. **Package Dependency**
- - Add `@timesafari/daily-notification-plugin` to `package.json`
- - Verify package availability/version
- - Document in dependencies section
+ - [ ] Add `@timesafari/daily-notification-plugin` to `package.json`
+ - [ ] Verify package availability/version
+ - [ ] Document in dependencies section
2. **PlatformService Interface Extension**
- - Add notification methods to `PlatformService` interface
- - Define notification types/interfaces (NotificationStatus, ScheduleOptions, etc.)
- - Implement in `CapacitorPlatformService` using `@timesafari/daily-notification-plugin`
- - Implement in `WebPlatformService` with null returns / error throws
- - Implement in `ElectronPlatformService` with null returns / error throws
+ - [ ] Add notification methods to `PlatformService` interface
+ - [ ] Define notification types/interfaces (NotificationStatus, ScheduleOptions, etc.)
+ - [ ] Implement in `CapacitorPlatformService` using `@timesafari/daily-notification-plugin`
+ - [ ] Implement in `WebPlatformService` with null returns / error throws
+ - [ ] Implement in `ElectronPlatformService` with null returns / error throws
3. **Settings Schema Extension**
- - Add notification settings fields to Settings interface
- - Update settings persistence methods if needed
+ - [ ] Add notification settings fields to Settings interface
+ - [ ] Update settings persistence methods if needed
#### Acceptance Criteria
- [ ] PlatformService interface extended with notification methods
@@ -644,25 +644,25 @@ interface Settings {
#### Tasks
1. **Supporting Components (Optional)**
- - Create supporting components only if AccountViewView exceeds length limits
- - Consider: `NotificationToggle.vue`, `NotificationTimePicker.vue`, `NotificationStatusDisplay.vue`
- - Follow project styling patterns
- - Add TypeScript interfaces
- - Keep components focused and reusable within AccountViewView context
+ - [ ] Create supporting components only if AccountViewView exceeds length limits
+ - [ ] Consider: `NotificationToggle.vue`, `NotificationTimePicker.vue`, `NotificationStatusDisplay.vue`
+ - [ ] Follow project styling patterns
+ - [ ] Add TypeScript interfaces
+ - [ ] Keep components focused and reusable within AccountViewView context
2. **AccountViewView Integration** ✅ **ACCEPTED**
- - Add separate "Daily Notifications" section
- - Check platform capabilities before showing UI (`v-if="notificationsSupported"`)
- - Add computed property for platform capability detection
- - Add toggle switch for enabling/disabling notifications
- - Add HTML5 time input for scheduling time
- - Integrate with PlatformService via PlatformServiceFactory
- - Save/load settings from `settings` table
- - Implement time format conversion (display vs storage)
- - Add enable/disable notification methods
- - Add edit time functionality
- - Add permission request flow
- - Add error handling and user feedback
+ - [ ] Add separate "Daily Notifications" section
+ - [ ] Check platform capabilities before showing UI (`v-if="notificationsSupported"`)
+ - [ ] Add computed property for platform capability detection
+ - [ ] Add toggle switch for enabling/disabling notifications
+ - [ ] Add HTML5 time input for scheduling time
+ - [ ] Integrate with PlatformService via PlatformServiceFactory
+ - [ ] Save/load settings from `settings` table
+ - [ ] Implement time format conversion (display vs storage)
+ - [ ] Add enable/disable notification methods
+ - [ ] Add edit time functionality
+ - [ ] Add permission request flow
+ - [ ] Add error handling and user feedback
#### Acceptance Criteria
- [ ] Supporting components created only if AccountViewView exceeds length limits
@@ -686,22 +686,22 @@ interface Settings {
#### Tasks
1. **Permission Management**
- - Implement permission request flow in AccountViewView
- - Handle permission denial gracefully
- - Update status after permission changes
- - Show appropriate user feedback
+ - [ ] Implement permission request flow in AccountViewView
+ - [ ] Handle permission denial gracefully
+ - [ ] Update status after permission changes
+ - [ ] Show appropriate user feedback
2. **Error Handling & User Feedback**
- - Add comprehensive error handling for all plugin operations
- - Implement loading states during async operations
- - Add success/error toast notifications
- - Handle edge cases (permission denied, plugin unavailable, etc.)
+ - [ ] Add comprehensive error handling for all plugin operations
+ - [ ] Implement loading states during async operations
+ - [ ] Add success/error toast notifications
+ - [ ] Handle edge cases (permission denied, plugin unavailable, etc.)
3. **Testing & Validation**
- - Test AccountViewView integration on Capacitor platforms
- - Verify component hiding on Web/Electron
- - Test all user workflows (enable, disable, edit time)
- - Verify settings persistence
+ - [ ] Test AccountViewView integration on Capacitor platforms
+ - [ ] Verify component hiding on Web/Electron
+ - [ ] Test all user workflows (enable, disable, edit time)
+ - [ ] Verify settings persistence
#### Acceptance Criteria
- [ ] Permission requests handled properly
@@ -720,53 +720,53 @@ interface Settings {
### Milestone 1: Foundation Complete
**Success Criteria**:
-- PlatformService interface extended
-- Settings schema extended
-- No build regressions
+- [ ] PlatformService interface extended
+- [ ] Settings schema extended
+- [ ] No build regressions
### Milestone 2: AccountViewView Integration Complete
**Success Criteria**:
-- AccountViewView notification section functional
-- Plugin integration working
-- Settings persistence working
-- Component hiding verified on unsupported platforms
+- [ ] AccountViewView notification section functional
+- [ ] Plugin integration working
+- [ ] Settings persistence working
+- [ ] Component hiding verified on unsupported platforms
### Milestone 3: Production Ready
**Success Criteria**:
-- All tests passing
-- Cross-platform validation complete
-- Error handling robust
-- User feedback implemented
-- Documentation complete
+- [ ] All tests passing
+- [ ] Cross-platform validation complete
+- [ ] Error handling robust
+- [ ] User feedback implemented
+- [ ] Documentation complete
---
## Testing Strategy
### Unit Tests
-- PlatformService platform detection
-- AccountViewView notification section rendering
-- Supporting component rendering (if created)
+- [ ] PlatformService platform detection
+- [ ] AccountViewView notification section rendering
+- [ ] Supporting component rendering (if created)
### Integration Tests
-- Plugin API calls from AccountViewView
-- Permission flows
-- Status updates
-- AccountViewView enable/disable/edit workflows
-- Settings persistence
+- [ ] Plugin API calls from AccountViewView
+- [ ] Permission flows
+- [ ] Status updates
+- [ ] AccountViewView enable/disable/edit workflows
+- [ ] Settings persistence
### Platform Tests
-- **Capacitor Android**: Notification scheduling, permissions, status, AccountViewView UI
-- **Capacitor iOS**: Notification scheduling, permissions, status, AccountViewView UI
-- **Web**: Feature hidden, no errors, AccountViewView section hidden
-- **Electron**: Feature hidden, no errors, AccountViewView section hidden
+- [ ] **Capacitor Android**: Notification scheduling, permissions, status, AccountViewView UI
+- [ ] **Capacitor iOS**: Notification scheduling, permissions, status, AccountViewView UI
+- [ ] **Web**: Feature hidden, no errors, AccountViewView section hidden
+- [ ] **Electron**: Feature hidden, no errors, AccountViewView section hidden
### E2E Tests (Playwright)
-- AccountViewView notification configuration workflow
-- Permission request flow
-- Enable/disable notification workflow
-- Edit notification time workflow
-- Error handling scenarios
+- [ ] AccountViewView notification configuration workflow
+- [ ] Permission request flow
+- [ ] Enable/disable notification workflow
+- [ ] Edit notification time workflow
+- [ ] Error handling scenarios
---
@@ -899,37 +899,37 @@ await platformService.scheduleDailyNotification({
### Risk 1: Plugin Package Unavailable
**Mitigation**:
-- Verify package exists and is accessible
-- Consider local development setup if needed
-- Document package installation requirements
+- [ ] Verify package exists and is accessible
+- [ ] Consider local development setup if needed
+- [ ] Document package installation requirements
### Risk 2: Platform Detection Failures
**Mitigation**:
-- Use proven patterns from `QRScannerFactory`
-- Test on all platforms
-- Add fallback logic
+- [ ] Use proven patterns from `QRScannerFactory`
+- [ ] Test on all platforms
+- [ ] Add fallback logic
### Risk 3: Web/Electron Build Breaks
**Mitigation**:
-- Use dynamic imports exclusively
-- Test web/electron builds after each phase
-- Ensure no static plugin imports
-- Verify AccountViewView section properly hidden
+- [ ] Use dynamic imports exclusively
+- [ ] Test web/electron builds after each phase
+- [ ] Ensure no static plugin imports
+- [ ] Verify AccountViewView section properly hidden
### Risk 4: AccountViewView Integration Issues
**Mitigation**:
-- Use platform capability detection before showing UI
-- Test on all platforms to ensure proper hiding
-- Follow existing UI patterns for consistency
-- Add comprehensive error handling
+- [ ] Use platform capability detection before showing UI
+- [ ] Test on all platforms to ensure proper hiding
+- [ ] Follow existing UI patterns for consistency
+- [ ] Add comprehensive error handling
### Risk 5: Components Visible on Unsupported Platforms
**Mitigation**:
-- **REQUIRED**: All scheduling components must check `getDailyNotificationStatus()` and hide if `null`
-- Use `v-if="notificationsSupported"` pattern consistently
-- Add explicit verification in acceptance criteria
-- Test on Web/Electron builds to verify hiding works
-- Document required pattern in Implementation Notes section
+- [ ] **REQUIRED**: All scheduling components must check `getDailyNotificationStatus()` and hide if `null`
+- [ ] Use `v-if="notificationsSupported"` pattern consistently
+- [ ] Add explicit verification in acceptance criteria
+- [ ] Test on Web/Electron builds to verify hiding works
+- [ ] Document required pattern in Implementation Notes section
---
@@ -952,11 +952,11 @@ await platformService.scheduleDailyNotification({
## Next Steps
-1. **Verify Plugin Package**: Confirm `@timesafari/daily-notification-plugin` availability
-2. **Extend PlatformService**: Add notification methods to PlatformService interface and implement in all platform services
-3. **Extend Settings Schema**: Add notification fields to Settings interface
-4. **Begin Phase 1 Implementation**: Start with foundation tasks
-5. **AccountViewView Integration**: Implement Daily Notifications section in Phase 2
+- [ ] **Verify Plugin Package**: Confirm `@timesafari/daily-notification-plugin` availability
+- [ ] **Extend PlatformService**: Add notification methods to PlatformService interface and implement in all platform services
+- [ ] **Extend Settings Schema**: Add notification fields to Settings interface
+- [ ] **Begin Phase 1 Implementation**: Start with foundation tasks
+- [ ] **AccountViewView Integration**: Implement Daily Notifications section in Phase 2
---
--
2.30.2
From 45eff4a9ac46e104ce1c2895cb83e860746c60e6 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 06:34:53 +0000
Subject: [PATCH 08/17] docs: add plugin state sync, time update logic, and
component extraction
- Update initialization to sync with plugin state on mount (checks for pre-existing schedules)
- Add updateNotificationTime() method to update schedule when time changes (cancel old, schedule new)
- Extract DailyNotificationSection into dedicated component using vue-facing-decorator
- Update component architecture to show DailyNotificationSection.vue structure
- Update Phase 2 tasks to reflect component creation and AccountViewView integration
- Add acceptance criteria for plugin state sync and time update functionality
- Update verification checklist with new requirements
---
...ly-notification-plugin-integration-plan.md | 211 ++++++++++++++----
1 file changed, 166 insertions(+), 45 deletions(-)
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index 58606380..4637554f 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -255,18 +255,58 @@ export interface NativeFetcherConfig {
#### Views Structure
```
src/views/
- └── AccountViewView.vue (existing - add Daily Notifications section)
+ └── AccountViewView.vue (existing - add DailyNotificationSection component)
```
-#### Supporting Components (Optional - Only if AccountViewView needs extraction)
+#### Supporting Components
```
-src/components/notifications/ (optional)
- ├── NotificationToggle.vue (optional - extract toggle if AccountViewView too long)
- ├── NotificationTimePicker.vue (optional - extract time picker if needed)
- └── NotificationStatusDisplay.vue (optional - extract status display if needed)
+src/components/notifications/
+ └── DailyNotificationSection.vue (required - extracted section component)
```
-**Note**: Supporting components should only be created if AccountViewView exceeds reasonable length limits (>200 lines). Keep everything in AccountViewView if possible.
+**Component Structure**: `DailyNotificationSection.vue` will use vue-facing-decorator with ES6 classes
+
+```vue
+
+
+
+
+
+```
---
@@ -422,7 +462,7 @@ function getTimeFromInput(timeInput: string): string {
### Data Flow
-#### 1. Initialization
+#### 1. Initialization (Sync with Plugin State)
```typescript
async initializeState() {
@@ -440,13 +480,33 @@ async initializeState() {
this.notificationsSupported = true;
- // Load from settings
- const nativeNotificationTime = settings.nativeNotificationTime || "";
- this.nativeNotificationEnabled = !!nativeNotificationTime;
- this.nativeNotificationTimeStorage = nativeNotificationTime;
-
- if (nativeNotificationTime) {
- this.nativeNotificationTime = formatTimeForDisplay(nativeNotificationTime);
+ // CRITICAL: Sync with plugin state first (source of truth)
+ // Plugin may have an existing schedule even if settings don't
+ if (status.isScheduled && status.scheduledTime) {
+ // Plugin has a scheduled notification - sync UI to match
+ this.nativeNotificationEnabled = true;
+ this.nativeNotificationTimeStorage = status.scheduledTime;
+ this.nativeNotificationTime = formatTimeForDisplay(status.scheduledTime);
+
+ // Also sync settings to match plugin state
+ const settings = await this.$accountSettings();
+ if (settings.nativeNotificationTime !== status.scheduledTime) {
+ await this.$saveSettings({
+ nativeNotificationTime: status.scheduledTime,
+ nativeNotificationTitle: settings.nativeNotificationTitle || this.nativeNotificationTitle,
+ nativeNotificationMessage: settings.nativeNotificationMessage || this.nativeNotificationMessage,
+ });
+ }
+ } else {
+ // No plugin schedule - check settings for user preference
+ const settings = await this.$accountSettings();
+ const nativeNotificationTime = settings.nativeNotificationTime || "";
+ this.nativeNotificationEnabled = !!nativeNotificationTime;
+ this.nativeNotificationTimeStorage = nativeNotificationTime;
+
+ if (nativeNotificationTime) {
+ this.nativeNotificationTime = formatTimeForDisplay(nativeNotificationTime);
+ }
}
// Update UI with current status
@@ -454,6 +514,12 @@ async initializeState() {
}
```
+**Key Points**:
+- `getDailyNotificationStatus()` is called on mount to check for pre-existing schedules
+- Plugin state is the source of truth - if plugin has a schedule, UI syncs to match
+- Settings are synced with plugin state if they differ
+- If no plugin schedule exists, fall back to settings
+
#### 2. Enable Notification
```typescript
@@ -529,18 +595,63 @@ async disableNativeNotification() {
}
```
-#### 4. Edit Time
+#### 4. Edit Time (Update Schedule)
-**Approach**: Use inline HTML5 time input for quick edits
+**Approach**: When time changes, immediately update the scheduled notification
```typescript
async editNativeNotificationTime() {
// Show inline HTML5 time input for quick changes
this.showTimeEdit = true;
}
+
+async updateNotificationTime(newTime: string) {
+ // newTime is in "HH:mm" format from HTML5 time input
+ if (!this.nativeNotificationEnabled) {
+ // If notification is disabled, just save the time preference
+ this.nativeNotificationTimeStorage = newTime;
+ this.nativeNotificationTime = formatTimeForDisplay(newTime);
+ await this.$saveSettings({
+ nativeNotificationTime: newTime,
+ });
+ return;
+ }
+
+ // Notification is enabled - update the schedule
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // 1. Cancel existing notification
+ await platformService.cancelDailyNotification();
+
+ // 2. Schedule with new time
+ await platformService.scheduleDailyNotification({
+ time: newTime, // "09:00" in local time
+ title: this.nativeNotificationTitle,
+ body: this.nativeNotificationMessage,
+ sound: true,
+ priority: 'high'
+ });
+
+ // 3. Update local state
+ this.nativeNotificationTimeStorage = newTime;
+ this.nativeNotificationTime = formatTimeForDisplay(newTime);
+
+ // 4. Save to settings
+ await this.$saveSettings({
+ nativeNotificationTime: newTime,
+ });
+
+ this.notify.success("Notification time updated successfully", TIMEOUTS.SHORT);
+ this.showTimeEdit = false;
+ } catch (error) {
+ logger.error("Failed to update notification time:", error);
+ this.notify.error("Failed to update notification time. Please try again.", TIMEOUTS.LONG);
+ }
+}
```
-**Implementation Note**: HTML5 time input provides native mobile picker experience when shown inline, making it ideal for quick time adjustments in AccountViewView.
+**Implementation Note**: HTML5 time input provides native mobile picker experience when shown inline, making it ideal for quick time adjustments. When the time changes, the notification schedule is immediately updated via PlatformService.
### Settings Schema
@@ -643,39 +754,46 @@ interface Settings {
**Goals**: Integrate notification scheduling into AccountViewView with optional supporting components
#### Tasks
-1. **Supporting Components (Optional)**
- - [ ] Create supporting components only if AccountViewView exceeds length limits
- - [ ] Consider: `NotificationToggle.vue`, `NotificationTimePicker.vue`, `NotificationStatusDisplay.vue`
- - [ ] Follow project styling patterns
- - [ ] Add TypeScript interfaces
- - [ ] Keep components focused and reusable within AccountViewView context
-
-2. **AccountViewView Integration** ✅ **ACCEPTED**
- - [ ] Add separate "Daily Notifications" section
- - [ ] Check platform capabilities before showing UI (`v-if="notificationsSupported"`)
- - [ ] Add computed property for platform capability detection
+1. **DailyNotificationSection Component**
+ - [ ] Create `src/components/notifications/DailyNotificationSection.vue`
+ - [ ] Use vue-facing-decorator with ES6 class extending Vue
+ - [ ] Add PlatformServiceMixin to component
+ - [ ] Implement platform capability detection on mount
+ - [ ] Implement initialization that syncs with plugin state (checks for pre-existing schedules)
- [ ] Add toggle switch for enabling/disabling notifications
- [ ] Add HTML5 time input for scheduling time
- [ ] Integrate with PlatformService via PlatformServiceFactory
- - [ ] Save/load settings from `settings` table
- [ ] Implement time format conversion (display vs storage)
- [ ] Add enable/disable notification methods
- - [ ] Add edit time functionality
+ - [ ] Add edit time functionality with schedule update (cancel old, schedule new)
- [ ] Add permission request flow
- [ ] Add error handling and user feedback
+ - [ ] Save/load settings from `settings` table
+ - [ ] Follow project styling patterns
+ - [ ] Add TypeScript interfaces
+ - [ ] Add file-level documentation
+
+2. **AccountViewView Integration**
+ - [ ] Import DailyNotificationSection component
+ - [ ] Add component to template (minimal integration)
+ - [ ] Verify component renders correctly
+ - [ ] Test component hiding on unsupported platforms
#### Acceptance Criteria
-- [ ] Supporting components created only if AccountViewView exceeds length limits
-- [ ] AccountViewView has separate "Daily Notifications" section
-- [ ] **AccountViewView notification section hidden on unsupported platforms** (`v-if="notificationsSupported"`)
-- [ ] Notification section checks PlatformService capabilities before showing
-- [ ] Toggle and time input functional in AccountViewView
+- [ ] DailyNotificationSection component created using vue-facing-decorator
+- [ ] Component extends Vue class with PlatformServiceMixin
+- [ ] Component checks platform support on mount via `getDailyNotificationStatus()`
+- [ ] Component syncs with plugin state on initialization (checks for pre-existing schedules)
+- [ ] Component hidden on unsupported platforms (`v-if="notificationsSupported"`)
+- [ ] Toggle and time input functional
+- [ ] Time changes update notification schedule immediately (cancel old, schedule new)
- [ ] Settings persist across app restarts
-- [ ] Plugin state syncs with settings
+- [ ] Plugin state syncs with settings on mount
- [ ] All logging uses project logger
- [ ] Error handling implemented
- [ ] Loading states visible
- [ ] UI matches existing design patterns
+- [ ] AccountViewView integration is minimal (just imports and uses component)
---
@@ -837,16 +955,16 @@ async mounted() {
#### Components That Must Implement This Pattern
-1. **AccountViewView.vue**: Daily Notifications section uses `v-if="notificationsSupported"`
-2. **Supporting components** (if created): Must check support before rendering any scheduling UI
-3. **Any component providing scheduling UI**: Must verify `getDailyNotificationStatus() !== null` before showing scheduling controls
+1. **DailyNotificationSection.vue**: Daily Notifications section uses `v-if="notificationsSupported"` and checks `getDailyNotificationStatus()` on mount
+2. **Any component providing scheduling UI**: Must verify `getDailyNotificationStatus() !== null` before showing scheduling controls
#### Verification Checklist
-- [ ] AccountViewView notification section hidden via `v-if` on unsupported platforms
-- [ ] Supporting components (if created) check and hide on unsupported platforms
-- [ ] All components tested on Web/Electron to verify hiding works
+- [ ] DailyNotificationSection checks platform support on mount and hides on unsupported platforms
+- [ ] DailyNotificationSection syncs with plugin state on initialization (checks for pre-existing schedules)
+- [ ] Component tested on Web/Electron to verify hiding works
- [ ] No console errors when components are hidden
+- [ ] Time changes properly update notification schedule
### Code Quality Standards
- **Logging**: Use `logger` from `@/utils/logger`, not `console.*`
@@ -938,8 +1056,11 @@ await platformService.scheduleDailyNotification({
- [ ] Plugin integrated using PlatformService architecture
- [ ] Feature works on Capacitor (Android/iOS)
- [ ] Feature hidden/graceful on Web/Electron
-- [ ] AccountViewView integration complete and functional
-- [ ] **AccountViewView notification section hides itself on unsupported platforms**
+- [ ] DailyNotificationSection component created and functional
+- [ ] **DailyNotificationSection hides itself on unsupported platforms**
+- [ ] Component syncs with plugin state on mount (checks for pre-existing schedules)
+- [ ] Time changes update notification schedule immediately
+- [ ] AccountViewView integration minimal (just imports component)
- [ ] Settings persist across app restarts
- [ ] Logging standardized (no console.*)
- [ ] Error handling robust
--
2.30.2
From 5def44c3496d202a6976f51ed1b483a81d944fd6 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 10:36:00 +0000
Subject: [PATCH 09/17] fix(db): remove inline comments from migration 006 SQL
Migration 006 was failing during database initialization because the SQLite
plugin splits SQL statements on semicolons, and inline comments after
semicolons were being treated as separate statements. When the last statement
was comment-only (e.g., '-- Notification body text'), it caused an error.
Fixed by removing all inline comments from the migration SQL. The comments
are already documented in the TypeScript code, so they're not needed in the
SQL itself.
NOTE: We're experiencing database initialization problems with storing
notification schedule data. The daily notification plugin was originally
designed to store the schedule internally, which would be a better approach
than storing it in our SQLite database. We should consider migrating to
using the plugin's internal storage instead of adding these columns to the
settings table.
---
src/db-sql/migration.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts
index 4a177786..7db68437 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -199,6 +199,14 @@ const MIGRATIONS = [
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
`,
},
+ {
+ name: "006_add_nativeNotificationSettings_to_settings",
+ sql: `
+ ALTER TABLE settings ADD COLUMN nativeNotificationTime TEXT;
+ ALTER TABLE settings ADD COLUMN nativeNotificationTitle TEXT;
+ ALTER TABLE settings ADD COLUMN nativeNotificationMessage TEXT;
+ `,
+ },
];
/**
--
2.30.2
From 5f17f6cb4eda26f1b2aa8098efa6971d19c73f18 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Wed, 5 Nov 2025 10:37:02 +0000
Subject: [PATCH 10/17] feat(notifications): integrate daily notification
plugin into AccountViewView
- Add notification methods to PlatformService interface
- Implement notification support in CapacitorPlatformService with plugin integration
- Add stub implementations in WebPlatformService and ElectronPlatformService
- Add nativeNotificationTime, nativeNotificationTitle, and nativeNotificationMessage fields to Settings interface
- Create DailyNotificationSection component for AccountViewView integration
- Add Android manifest permissions (POST_NOTIFICATIONS, SCHEDULE_EXACT_ALARM, RECEIVE_BOOT_COMPLETED)
- Register daily-notification-plugin in capacitor.plugins.json
- Integrate DailyNotificationSection into AccountViewView
Features:
- Platform capability detection (hides on unsupported platforms)
- Permission request flow with fallback to settings
- Schedule/cancel notifications
- Time editing with HTML5 time input
- Settings persistence
- Plugin state synchronization on app load
NOTE: Currently storing notification schedule in SQLite database, but plugin
was designed to store schedule internally. Consider migrating to plugin's
internal storage to avoid database initialization issues.
---
android/app/src/main/AndroidManifest.xml | 11 +
.../src/main/assets/capacitor.plugins.json | 4 +
.../DailyNotificationSection.vue | 676 ++++++++++++++++++
src/db/tables/settings.ts | 5 +
src/services/PlatformService.ts | 114 +++
.../platforms/CapacitorPlatformService.ts | 358 ++++++++++
.../platforms/ElectronPlatformService.ts | 91 +++
src/services/platforms/WebPlatformService.ts | 82 +++
src/views/AccountViewView.vue | 5 +
9 files changed, 1346 insertions(+)
create mode 100644 src/components/notifications/DailyNotificationSection.vue
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1d8ad70d..3aa15877 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -45,4 +45,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index 721bea0d..d89dc7dd 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -35,6 +35,10 @@
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
},
+ {
+ "pkg": "@timesafari/daily-notification-plugin",
+ "classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
+ },
{
"pkg": "@timesafari/daily-notification-plugin",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
diff --git a/src/components/notifications/DailyNotificationSection.vue b/src/components/notifications/DailyNotificationSection.vue
new file mode 100644
index 00000000..48533a95
--- /dev/null
+++ b/src/components/notifications/DailyNotificationSection.vue
@@ -0,0 +1,676 @@
+
+
+
+ Daily Notifications
+
+
+
+
+
Daily Notification
+
+
+
+
+
+
+
+
+ Enable notifications in Settings > App info > Notifications
+
+
+
+
+
+
+ Scheduled for:
+ {{
+ nativeNotificationTime
+ }}
+ Not set
+
+
+
+
+
+
+
+
+
+
+
+ Set a time before enabling notifications
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index 493e4596..0af24058 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -53,6 +53,11 @@ export type Settings = {
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
+ // Native notification settings (Capacitor only)
+ nativeNotificationTime?: string; // "09:00" format (24-hour) - scheduled time for daily notification
+ nativeNotificationTitle?: string; // Default: "Daily Update" - notification title
+ nativeNotificationMessage?: string; // Default message - notification body text
+
partnerApiServer?: string; // partner server API URL
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts
index a8ae9ee7..6db1c04d 100644
--- a/src/services/PlatformService.ts
+++ b/src/services/PlatformService.ts
@@ -32,6 +32,68 @@ export interface PlatformCapabilities {
isNativeApp: boolean;
}
+/**
+ * Permission status for notifications
+ */
+export interface PermissionStatus {
+ /** Notification permission status */
+ notifications: "granted" | "denied" | "prompt";
+ /** Exact alarms permission status (Android only) */
+ exactAlarms?: "granted" | "denied" | "prompt";
+}
+
+/**
+ * Result of permission request
+ */
+export interface PermissionResult {
+ /** Whether notification permission was granted */
+ notifications: boolean;
+ /** Whether exact alarms permission was granted (Android only) */
+ exactAlarms?: boolean;
+}
+
+/**
+ * Status of scheduled daily notifications
+ */
+export interface NotificationStatus {
+ /** Whether a notification is currently scheduled */
+ isScheduled: boolean;
+ /** Scheduled time in "HH:mm" format (24-hour) */
+ scheduledTime?: string;
+ /** Last time the notification was triggered (ISO string) */
+ lastTriggered?: string;
+ /** Current permission status */
+ permissions: PermissionStatus;
+}
+
+/**
+ * Options for scheduling a daily notification
+ */
+export interface ScheduleOptions {
+ /** Time in "HH:mm" format (24-hour) in local time */
+ time: string;
+ /** Notification title */
+ title: string;
+ /** Notification body text */
+ body: string;
+ /** Whether to play sound (default: true) */
+ sound?: boolean;
+ /** Notification priority */
+ priority?: "high" | "normal" | "low";
+}
+
+/**
+ * Configuration for native fetcher background operations
+ */
+export interface NativeFetcherConfig {
+ /** API server URL */
+ apiServer: string;
+ /** JWT token for authentication */
+ jwt: string;
+ /** Array of starred plan handle IDs */
+ starredPlanHandleIds: string[];
+}
+
/**
* Platform-agnostic interface for handling platform-specific operations.
* Provides a common API for file system operations, camera interactions,
@@ -209,6 +271,58 @@ export interface PlatformService {
*/
retrieveSettingsForActiveAccount(): Promise | null>;
+ // Daily notification operations
+ /**
+ * Get the status of scheduled daily notifications
+ * @returns Promise resolving to notification status, or null if not supported
+ */
+ getDailyNotificationStatus(): Promise;
+
+ /**
+ * Check notification permissions
+ * @returns Promise resolving to permission status, or null if not supported
+ */
+ checkNotificationPermissions(): Promise;
+
+ /**
+ * Request notification permissions
+ * @returns Promise resolving to permission result, or null if not supported
+ */
+ requestNotificationPermissions(): Promise;
+
+ /**
+ * Schedule a daily notification
+ * @param options - Notification scheduling options
+ * @returns Promise that resolves when scheduled, or rejects if not supported
+ */
+ scheduleDailyNotification(options: ScheduleOptions): Promise;
+
+ /**
+ * Cancel scheduled daily notification
+ * @returns Promise that resolves when cancelled, or rejects if not supported
+ */
+ cancelDailyNotification(): Promise;
+
+ /**
+ * Configure native fetcher for background operations
+ * @param config - Native fetcher configuration
+ * @returns Promise that resolves when configured, or null if not supported
+ */
+ configureNativeFetcher(config: NativeFetcherConfig): Promise;
+
+ /**
+ * Update starred plans for background fetcher
+ * @param plans - Starred plan IDs
+ * @returns Promise that resolves when updated, or null if not supported
+ */
+ updateStarredPlans(plans: { planIds: string[] }): Promise;
+
+ /**
+ * Open the app's notification settings in the system settings
+ * @returns Promise that resolves when the settings page is opened, or null if not supported
+ */
+ openAppNotificationSettings(): Promise;
+
// --- PWA/Web-only methods (optional, only implemented on web) ---
/**
* Registers the service worker for PWA support (web only)
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 51fb9ce5..30b8d8b4 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -20,6 +20,11 @@ import {
ImageResult,
PlatformService,
PlatformCapabilities,
+ NotificationStatus,
+ PermissionStatus,
+ PermissionResult,
+ ScheduleOptions,
+ NativeFetcherConfig,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { BaseDatabaseService } from "./BaseDatabaseService";
@@ -1333,6 +1338,359 @@ export class CapacitorPlatformService
// --- PWA/Web-only methods (no-op for Capacitor) ---
public registerServiceWorker(): void {}
+ // Daily notification operations
+ /**
+ * Get the status of scheduled daily notifications
+ * @see PlatformService.getDailyNotificationStatus
+ */
+ async getDailyNotificationStatus(): Promise {
+ try {
+ // Dynamic import to avoid build issues if plugin unavailable
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ const pluginStatus = await DailyNotification.getNotificationStatus();
+
+ // Get permissions separately
+ const permissions = await DailyNotification.checkPermissions();
+
+ // Map plugin PermissionState to our PermissionStatus format
+ const notificationsPermission = permissions.notifications;
+ let notifications: "granted" | "denied" | "prompt";
+
+ if (notificationsPermission === "granted") {
+ notifications = "granted";
+ } else if (notificationsPermission === "denied") {
+ notifications = "denied";
+ } else {
+ notifications = "prompt";
+ }
+
+ // Handle lastNotificationTime which can be a Promise
+ let lastTriggered: string | undefined;
+ const lastNotificationTime = pluginStatus.lastNotificationTime;
+ if (lastNotificationTime) {
+ const timeValue = await Promise.resolve(lastNotificationTime);
+ if (typeof timeValue === "number") {
+ lastTriggered = new Date(timeValue).toISOString();
+ }
+ }
+
+ return {
+ isScheduled: pluginStatus.isScheduled ?? false,
+ scheduledTime: pluginStatus.settings?.time,
+ lastTriggered,
+ permissions: {
+ notifications,
+ exactAlarms: undefined, // Plugin doesn't expose this in status
+ },
+ };
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to get notification status:",
+ error,
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Check notification permissions
+ * @see PlatformService.checkNotificationPermissions
+ */
+ async checkNotificationPermissions(): Promise {
+ try {
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ const permissions = await DailyNotification.checkPermissions();
+
+ // Log the raw permission state for debugging
+ logger.info(
+ `[CapacitorPlatformService] Raw permission state from plugin:`,
+ permissions,
+ );
+
+ // Map plugin PermissionState to our PermissionStatus format
+ const notificationsPermission = permissions.notifications;
+ let notifications: "granted" | "denied" | "prompt";
+
+ // Handle all possible PermissionState values
+ if (notificationsPermission === "granted") {
+ notifications = "granted";
+ } else if (
+ notificationsPermission === "denied" ||
+ notificationsPermission === "ephemeral"
+ ) {
+ notifications = "denied";
+ } else {
+ // Treat "prompt", "prompt-with-rationale", "unknown", "provisional" as "prompt"
+ // This allows Android to show the permission dialog
+ notifications = "prompt";
+ }
+
+ logger.info(
+ `[CapacitorPlatformService] Mapped permission state: ${notifications} (from ${notificationsPermission})`,
+ );
+
+ return {
+ notifications,
+ exactAlarms: undefined, // Plugin doesn't expose this directly
+ };
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to check permissions:",
+ error,
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Request notification permissions
+ * @see PlatformService.requestNotificationPermissions
+ */
+ async requestNotificationPermissions(): Promise {
+ try {
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ logger.info(
+ `[CapacitorPlatformService] Requesting notification permissions...`,
+ );
+
+ const result = await DailyNotification.requestPermissions();
+
+ logger.info(
+ `[CapacitorPlatformService] Permission request result:`,
+ result,
+ );
+
+ // Map plugin PermissionState to boolean
+ const notificationsGranted = result.notifications === "granted";
+
+ logger.info(
+ `[CapacitorPlatformService] Mapped permission result: ${notificationsGranted} (from ${result.notifications})`,
+ );
+
+ return {
+ notifications: notificationsGranted,
+ exactAlarms: undefined, // Plugin doesn't expose this directly
+ };
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to request permissions:",
+ error,
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Schedule a daily notification
+ * @see PlatformService.scheduleDailyNotification
+ */
+ async scheduleDailyNotification(options: ScheduleOptions): Promise {
+ try {
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ await DailyNotification.scheduleDailyNotification({
+ time: options.time,
+ title: options.title,
+ body: options.body,
+ sound: options.sound ?? true,
+ priority: options.priority ?? "high",
+ });
+
+ logger.info(
+ `[CapacitorPlatformService] Scheduled daily notification for ${options.time}`,
+ );
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to schedule notification:",
+ error,
+ );
+ throw error;
+ }
+ }
+
+ /**
+ * Cancel scheduled daily notification
+ * @see PlatformService.cancelDailyNotification
+ */
+ async cancelDailyNotification(): Promise {
+ try {
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ await DailyNotification.cancelAllNotifications();
+
+ logger.info("[CapacitorPlatformService] Cancelled daily notification");
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to cancel notification:",
+ error,
+ );
+ throw error;
+ }
+ }
+
+ /**
+ * Configure native fetcher for background operations
+ * @see PlatformService.configureNativeFetcher
+ */
+ async configureNativeFetcher(
+ config: NativeFetcherConfig,
+ ): Promise {
+ try {
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ // Plugin expects apiBaseUrl, activeDid, and jwtToken
+ // We'll need to get activeDid from somewhere - for now pass empty string
+ // Components should provide activeDid when calling this
+ await DailyNotification.configureNativeFetcher({
+ apiBaseUrl: config.apiServer,
+ activeDid: "", // Should be provided by caller
+ jwtToken: config.jwt,
+ });
+
+ logger.info("[CapacitorPlatformService] Configured native fetcher");
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to configure native fetcher:",
+ error,
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Update starred plans for background fetcher
+ * @see PlatformService.updateStarredPlans
+ */
+ async updateStarredPlans(plans: { planIds: string[] }): Promise {
+ try {
+ const { DailyNotification } = await import(
+ "@timesafari/daily-notification-plugin"
+ );
+
+ await DailyNotification.updateStarredPlans({
+ planIds: plans.planIds,
+ });
+
+ logger.info(
+ `[CapacitorPlatformService] Updated starred plans: ${plans.planIds.length} plans`,
+ );
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to update starred plans:",
+ error,
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Open the app's notification settings in the system settings
+ * @see PlatformService.openAppNotificationSettings
+ */
+ async openAppNotificationSettings(): Promise {
+ try {
+ const platform = Capacitor.getPlatform();
+
+ if (platform === "android") {
+ // Android: Open app details settings page
+ // From there, users can navigate to "Notifications" section
+ // This is more reliable than trying to open notification settings directly
+ const packageName = "app.timesafari.app"; // Full application ID from build.gradle
+
+ // Use APPLICATION_DETAILS_SETTINGS which opens the app's settings page
+ // Users can then navigate to "Notifications" section
+ // Try multiple URL formats to ensure compatibility
+ const intentUrl1 = `intent:#Intent;action=android.settings.APPLICATION_DETAILS_SETTINGS;data=package:${packageName};end`;
+ const intentUrl2 = `intent://settings/app_detail?package=${packageName}#Intent;scheme=android-app;end`;
+
+ logger.info(
+ `[CapacitorPlatformService] Opening Android app settings for ${packageName}`,
+ );
+
+ // Log current permission state before opening settings
+ try {
+ const currentPerms = await this.checkNotificationPermissions();
+ logger.info(
+ `[CapacitorPlatformService] Current permission state before opening settings:`,
+ currentPerms,
+ );
+ } catch (e) {
+ logger.warn(
+ `[CapacitorPlatformService] Could not check permissions before opening settings:`,
+ e,
+ );
+ }
+
+ // Try multiple approaches to ensure it works
+ try {
+ // Method 1: Direct window.location.href (most reliable)
+ window.location.href = intentUrl1;
+
+ // Method 2: Fallback with window.open
+ setTimeout(() => {
+ try {
+ window.open(intentUrl1, "_blank");
+ } catch (e) {
+ logger.warn(
+ "[CapacitorPlatformService] window.open fallback failed:",
+ e,
+ );
+ }
+ }, 100);
+
+ // Method 3: Alternative format
+ setTimeout(() => {
+ try {
+ window.location.href = intentUrl2;
+ } catch (e) {
+ logger.warn(
+ "[CapacitorPlatformService] Alternative format failed:",
+ e,
+ );
+ }
+ }, 200);
+ } catch (e) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to open intent URL:",
+ e,
+ );
+ }
+ } else if (platform === "ios") {
+ // iOS: Use app settings URL scheme
+ const settingsUrl = `app-settings:`;
+ window.location.href = settingsUrl;
+
+ logger.info("[CapacitorPlatformService] Opening iOS app settings");
+ } else {
+ logger.warn(
+ `[CapacitorPlatformService] Cannot open settings on platform: ${platform}`,
+ );
+ return null;
+ }
+ } catch (error) {
+ logger.error(
+ "[CapacitorPlatformService] Failed to open app notification settings:",
+ error,
+ );
+ return null;
+ }
+ }
+
// Database utility methods - inherited from BaseDatabaseService
// generateInsertStatement, updateDefaultSettings, updateActiveDid,
// getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts
index 1a077c65..f7350b12 100644
--- a/src/services/platforms/ElectronPlatformService.ts
+++ b/src/services/platforms/ElectronPlatformService.ts
@@ -22,6 +22,13 @@
import { CapacitorPlatformService } from "./CapacitorPlatformService";
import { logger } from "../../utils/logger";
+import {
+ NotificationStatus,
+ PermissionStatus,
+ PermissionResult,
+ ScheduleOptions,
+ NativeFetcherConfig,
+} from "../PlatformService";
/**
* Electron-specific platform service implementation.
@@ -166,4 +173,88 @@ export class ElectronPlatformService extends CapacitorPlatformService {
// --- PWA/Web-only methods (no-op for Electron) ---
public registerServiceWorker(): void {}
+
+ // Daily notification operations
+ // Override CapacitorPlatformService methods to return null/throw errors
+ // since Electron doesn't support native daily notifications
+
+ /**
+ * Get the status of scheduled daily notifications
+ * @see PlatformService.getDailyNotificationStatus
+ * @returns null - notifications not supported on Electron platform
+ */
+ async getDailyNotificationStatus(): Promise {
+ return null;
+ }
+
+ /**
+ * Check notification permissions
+ * @see PlatformService.checkNotificationPermissions
+ * @returns null - notifications not supported on Electron platform
+ */
+ async checkNotificationPermissions(): Promise {
+ return null;
+ }
+
+ /**
+ * Request notification permissions
+ * @see PlatformService.requestNotificationPermissions
+ * @returns null - notifications not supported on Electron platform
+ */
+ async requestNotificationPermissions(): Promise {
+ return null;
+ }
+
+ /**
+ * Schedule a daily notification
+ * @see PlatformService.scheduleDailyNotification
+ * @throws Error - notifications not supported on Electron platform
+ */
+ async scheduleDailyNotification(_options: ScheduleOptions): Promise {
+ throw new Error(
+ "Daily notifications are not supported on Electron platform",
+ );
+ }
+
+ /**
+ * Cancel scheduled daily notification
+ * @see PlatformService.cancelDailyNotification
+ * @throws Error - notifications not supported on Electron platform
+ */
+ async cancelDailyNotification(): Promise {
+ throw new Error(
+ "Daily notifications are not supported on Electron platform",
+ );
+ }
+
+ /**
+ * Configure native fetcher for background operations
+ * @see PlatformService.configureNativeFetcher
+ * @returns null - native fetcher not supported on Electron platform
+ */
+ async configureNativeFetcher(
+ _config: NativeFetcherConfig,
+ ): Promise {
+ return null;
+ }
+
+ /**
+ * Update starred plans for background fetcher
+ * @see PlatformService.updateStarredPlans
+ * @returns null - native fetcher not supported on Electron platform
+ */
+ async updateStarredPlans(_plans: {
+ planIds: string[];
+ }): Promise {
+ return null;
+ }
+
+ /**
+ * Open the app's notification settings in the system settings
+ * @see PlatformService.openAppNotificationSettings
+ * @returns null - not supported on Electron platform
+ */
+ async openAppNotificationSettings(): Promise {
+ return null;
+ }
}
diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts
index da573837..5b4f83b5 100644
--- a/src/services/platforms/WebPlatformService.ts
+++ b/src/services/platforms/WebPlatformService.ts
@@ -2,6 +2,11 @@ import {
ImageResult,
PlatformService,
PlatformCapabilities,
+ NotificationStatus,
+ PermissionStatus,
+ PermissionResult,
+ ScheduleOptions,
+ NativeFetcherConfig,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
@@ -677,4 +682,81 @@ export class WebPlatformService
// generateInsertStatement, updateDefaultSettings, updateActiveDid,
// getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
// retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService
+
+ // Daily notification operations
+ /**
+ * Get the status of scheduled daily notifications
+ * @see PlatformService.getDailyNotificationStatus
+ * @returns null - notifications not supported on web platform
+ */
+ async getDailyNotificationStatus(): Promise {
+ return null;
+ }
+
+ /**
+ * Check notification permissions
+ * @see PlatformService.checkNotificationPermissions
+ * @returns null - notifications not supported on web platform
+ */
+ async checkNotificationPermissions(): Promise {
+ return null;
+ }
+
+ /**
+ * Request notification permissions
+ * @see PlatformService.requestNotificationPermissions
+ * @returns null - notifications not supported on web platform
+ */
+ async requestNotificationPermissions(): Promise {
+ return null;
+ }
+
+ /**
+ * Schedule a daily notification
+ * @see PlatformService.scheduleDailyNotification
+ * @throws Error - notifications not supported on web platform
+ */
+ async scheduleDailyNotification(_options: ScheduleOptions): Promise {
+ throw new Error("Daily notifications are not supported on web platform");
+ }
+
+ /**
+ * Cancel scheduled daily notification
+ * @see PlatformService.cancelDailyNotification
+ * @throws Error - notifications not supported on web platform
+ */
+ async cancelDailyNotification(): Promise {
+ throw new Error("Daily notifications are not supported on web platform");
+ }
+
+ /**
+ * Configure native fetcher for background operations
+ * @see PlatformService.configureNativeFetcher
+ * @returns null - native fetcher not supported on web platform
+ */
+ async configureNativeFetcher(
+ _config: NativeFetcherConfig,
+ ): Promise {
+ return null;
+ }
+
+ /**
+ * Update starred plans for background fetcher
+ * @see PlatformService.updateStarredPlans
+ * @returns null - native fetcher not supported on web platform
+ */
+ async updateStarredPlans(_plans: {
+ planIds: string[];
+ }): Promise {
+ return null;
+ }
+
+ /**
+ * Open the app's notification settings in the system settings
+ * @see PlatformService.openAppNotificationSettings
+ * @returns null - not supported on web platform
+ */
+ async openAppNotificationSettings(): Promise {
+ return null;
+ }
}
diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue
index 9b7efd3e..ab107e62 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -161,6 +161,9 @@
+
+
+
Date: Thu, 6 Nov 2025 12:44:06 +0000
Subject: [PATCH 11/17] feat: implement 72-hour JWT token refresh for daily
notification plugin
- Add accessTokenForBackground() with 72-hour default expiration
- Supports offline-first prefetch operations
- Balances security with offline capability
- Implement proactive token refresh strategy
- Refresh on component mount (DailyNotificationSection)
- Refresh on app resume (Capacitor 'resume' event)
- Refresh when notifications are enabled
- Automatic refresh without user interaction
- Update CapacitorPlatformService.configureNativeFetcher()
- Automatically retrieves activeDid from database
- Generates 72-hour JWT tokens for background operations
- Includes starred plans in configuration
- Add BroadcastReceivers to AndroidManifest.xml
- DailyNotificationReceiver for scheduled notifications
- BootReceiver for rescheduling after device reboot
- Add comprehensive documentation
- JSDoc comments for all token-related functions
- Inline comments explaining refresh strategy
- Documentation section on authentication & token management
Benefits:
- No app wake-up required (refresh when app already open)
- Works offline (72-hour validity supports extended periods)
- Automatic (no user interaction required)
- Graceful degradation (uses cached content if refresh fails)
---
android/app/src/main/AndroidManifest.xml | 24 +++
.../src/main/assets/capacitor.plugins.json | 4 -
...ly-notification-plugin-integration-plan.md | 59 ++++-
package-lock.json | 2 +-
.../DailyNotificationSection.vue | 202 +++++++++++++-----
src/db-sql/migration.ts | 8 -
src/db/tables/settings.ts | 5 -
src/libs/crypto/index.ts | 65 ++++++
.../platforms/CapacitorPlatformService.ts | 100 ++++++++-
vite.config.capacitor.mts | 21 +-
10 files changed, 410 insertions(+), 80 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3aa15877..6392c86b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,30 @@
android:grantUriPermissions="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index d89dc7dd..721bea0d 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -35,10 +35,6 @@
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
},
- {
- "pkg": "@timesafari/daily-notification-plugin",
- "classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
- },
{
"pkg": "@timesafari/daily-notification-plugin",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index 4637554f..35c8aa5a 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -241,7 +241,7 @@ export interface ScheduleOptions {
export interface NativeFetcherConfig {
apiServer: string;
- jwt: string;
+ jwt: string; // Ignored - generated automatically by configureNativeFetcher
starredPlanHandleIds: string[];
}
```
@@ -250,6 +250,63 @@ export interface NativeFetcherConfig {
- **Capacitor**: Full implementation, all methods functional
- **Web/Electron**: Status/permission methods return `null`, scheduling methods throw errors with clear messages
+### Authentication & Token Management
+
+#### Background Prefetch Authentication
+
+The daily notification plugin requires authentication tokens for background prefetch operations. The implementation uses a **hybrid token refresh strategy** that balances security with offline capability.
+
+**Token Generation** (`src/libs/crypto/index.ts`):
+- Function: `accessTokenForBackground(did, expirationMinutes?)`
+- Default expiration: **72 hours** (4320 minutes)
+- Token type: JWT with ES256K signing
+- Payload: `{ exp, iat, iss: did }`
+
+**Why 72 Hours?**
+- Balances security (read-only prefetch operations) with offline capability
+- Reduces need for app to wake itself for token refresh
+- Allows plugin to work offline for extended periods (e.g., weekend trips)
+- Longer than typical prefetch windows (5 minutes before notification)
+
+**Token Refresh Strategy (Hybrid Approach)**:
+
+1. **Proactive Refresh Triggers**:
+ - Component mount (`DailyNotificationSection.mounted()`)
+ - App resume (Capacitor `resume` event)
+ - Notification enabled (when user enables daily notifications)
+
+2. **Refresh Implementation** (`DailyNotificationSection.refreshNativeFetcherConfig()`):
+ - Checks if notifications are supported and enabled
+ - Retrieves API server URL from settings
+ - Retrieves starred plans from settings
+ - Calls `configureNativeFetcher()` to generate fresh token
+ - Errors are logged but don't interrupt user experience
+
+3. **Offline Behavior**:
+ - If token expires while offline → plugin uses cached content
+ - Next time app opens → token automatically refreshed
+ - No app wake-up required (refresh happens when app is already open)
+
+**Configuration Flow** (`CapacitorPlatformService.configureNativeFetcher()`):
+
+1. Retrieves active DID from `active_identity` table (single source of truth)
+2. Generates JWT token with 72-hour expiration using `accessTokenForBackground()`
+3. Configures plugin with API server URL, active DID, and JWT token
+4. Plugin stores token in its Room database for background workers
+
+**Security Considerations**:
+- Tokens are used only for read-only prefetch operations
+- Tokens are stored securely in plugin's Room database
+- Tokens are refreshed proactively to minimize exposure window
+- No private keys are exposed to native code
+- Token generation happens in TypeScript (no Java crypto compatibility issues)
+
+**Error Handling**:
+- Returns `null` if active DID not found (no user logged in)
+- Returns `null` if JWT generation fails
+- Logs errors but doesn't throw (allows graceful degradation)
+- Refresh failures don't interrupt user experience (plugin uses cached content)
+
### Component Architecture
#### Views Structure
diff --git a/package-lock.json b/package-lock.json
index dec7d1a5..1ca0e8a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -152,7 +152,7 @@
},
"../daily-notification-plugin": {
"name": "@timesafari/daily-notification-plugin",
- "version": "1.0.0",
+ "version": "1.0.1",
"license": "MIT",
"workspaces": [
"packages/*"
diff --git a/src/components/notifications/DailyNotificationSection.vue b/src/components/notifications/DailyNotificationSection.vue
index 48533a95..9858014d 100644
--- a/src/components/notifications/DailyNotificationSection.vue
+++ b/src/components/notifications/DailyNotificationSection.vue
@@ -206,9 +206,146 @@ export default class DailyNotificationSection extends Vue {
/**
* Initialize component state on mount
* Checks platform support and syncs with plugin state
+ *
+ * **Token Refresh on Mount:**
+ * - Refreshes native fetcher configuration to ensure plugin has valid token
+ * - This handles cases where app was closed for extended periods
+ * - Token refresh happens automatically without user interaction
+ *
+ * **App Resume Listener:**
+ * - Listens for Capacitor 'resume' event to refresh token when app comes to foreground
+ * - Ensures plugin always has fresh token for background prefetch operations
+ * - Cleaned up in `beforeDestroy()` lifecycle hook
*/
async mounted(): Promise {
await this.initializeState();
+ // Refresh native fetcher configuration on mount
+ // This ensures plugin has valid token even if app was closed for extended periods
+ await this.refreshNativeFetcherConfig();
+ // Listen for app resume events to refresh token when app comes to foreground
+ // This is part of the proactive token refresh strategy
+ document.addEventListener("resume", this.handleAppResume);
+ }
+
+ /**
+ * Cleanup on component destroy
+ */
+ beforeDestroy(): void {
+ document.removeEventListener("resume", this.handleAppResume);
+ }
+
+ /**
+ * Handle app resume event - refresh native fetcher configuration
+ *
+ * This method is called when the app comes to foreground (via Capacitor 'resume' event).
+ * It proactively refreshes the JWT token to ensure the plugin has valid authentication
+ * for background prefetch operations.
+ *
+ * **Why refresh on resume?**
+ * - Tokens expire after 72 hours
+ * - App may have been closed for extended periods
+ * - Refreshing ensures plugin has valid token for next prefetch cycle
+ * - No user interaction required - happens automatically
+ *
+ * @see {@link refreshNativeFetcherConfig} For implementation details
+ */
+ async handleAppResume(): Promise {
+ logger.debug(
+ "[DailyNotificationSection] App resumed, refreshing native fetcher config",
+ );
+ await this.refreshNativeFetcherConfig();
+ }
+
+ /**
+ * Refresh native fetcher configuration with fresh JWT token
+ *
+ * This method ensures the daily notification plugin has a valid authentication token
+ * for background prefetch operations. It's called proactively to prevent token expiration
+ * issues during offline periods.
+ *
+ * **Refresh Triggers:**
+ * - Component mount (when notification settings page loads)
+ * - App resume (when app comes to foreground)
+ * - Notification enabled (when user enables daily notifications)
+ *
+ * **Token Refresh Strategy (Hybrid Approach):**
+ * - Tokens are valid for 72 hours (see `accessTokenForBackground`)
+ * - Tokens are refreshed proactively when app is already open
+ * - If token expires while offline, plugin uses cached content
+ * - Next time app opens, token is automatically refreshed
+ *
+ * **Why This Approach?**
+ * - No app wake-up required (tokens refresh when app is already open)
+ * - Works offline (72-hour validity supports extended offline periods)
+ * - Automatic (no user interaction required)
+ * - Includes starred plans (fetcher receives user's starred plans for prefetch)
+ * - Graceful degradation (if refresh fails, cached content still works)
+ *
+ * **Error Handling:**
+ * - Errors are logged but not shown to user (background operation)
+ * - Returns early if notifications not supported or disabled
+ * - Returns early if API server not configured
+ * - Failures don't interrupt user experience
+ *
+ * @returns Promise that resolves when refresh completes (or fails silently)
+ *
+ * @example
+ * ```typescript
+ * // Called automatically on mount/resume
+ * await this.refreshNativeFetcherConfig();
+ * ```
+ *
+ * @see {@link CapacitorPlatformService.configureNativeFetcher} For token generation
+ * @see {@link accessTokenForBackground} For 72-hour token generation
+ */
+ async refreshNativeFetcherConfig(): Promise {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // Early return: Only refresh if notifications are supported and enabled
+ // This prevents unnecessary work when notifications aren't being used
+ if (!this.notificationsSupported || !this.nativeNotificationEnabled) {
+ return;
+ }
+
+ // Get settings for API server and starred plans
+ // API server tells plugin where to fetch content from
+ // Starred plans tell plugin which plans to prefetch
+ const settings = await this.$accountSettings();
+ const apiServer = settings.apiServer || "";
+
+ if (!apiServer) {
+ logger.warn(
+ "[DailyNotificationSection] No API server configured, skipping native fetcher refresh",
+ );
+ return;
+ }
+
+ // Get starred plans from settings
+ // These are passed to the plugin so it knows which plans to prefetch
+ const starredPlanHandleIds = settings.starredPlanHandleIds || [];
+
+ // Configure native fetcher with fresh token
+ // The jwt parameter is ignored - configureNativeFetcher generates it automatically
+ // This ensures we always have a fresh token with current expiration time
+ await platformService.configureNativeFetcher({
+ apiServer,
+ jwt: "", // Will be generated automatically by configureNativeFetcher
+ starredPlanHandleIds,
+ });
+
+ logger.info(
+ "[DailyNotificationSection] Native fetcher configuration refreshed",
+ );
+ } catch (error) {
+ // Don't show error to user - this is a background operation
+ // Failures are logged for debugging but don't interrupt user experience
+ // If refresh fails, plugin will use existing token (if still valid) or cached content
+ logger.error(
+ "[DailyNotificationSection] Failed to refresh native fetcher config:",
+ error,
+ );
+ }
}
/**
@@ -235,8 +372,7 @@ export default class DailyNotificationSection extends Vue {
this.notificationsSupported = true;
this.notificationStatus = status;
- // CRITICAL: Sync with plugin state first (source of truth)
- // Plugin may have an existing schedule even if settings don't
+ // Plugin state is the source of truth
if (status.isScheduled && status.scheduledTime) {
// Plugin has a scheduled notification - sync UI to match
this.nativeNotificationEnabled = true;
@@ -244,31 +380,11 @@ export default class DailyNotificationSection extends Vue {
this.nativeNotificationTime = formatTimeForDisplay(
status.scheduledTime,
);
-
- // Also sync settings to match plugin state
- const settings = await this.$accountSettings();
- if (settings.nativeNotificationTime !== status.scheduledTime) {
- await this.$saveSettings({
- nativeNotificationTime: status.scheduledTime,
- nativeNotificationTitle:
- settings.nativeNotificationTitle || this.nativeNotificationTitle,
- nativeNotificationMessage:
- settings.nativeNotificationMessage ||
- this.nativeNotificationMessage,
- });
- }
} else {
- // No plugin schedule - check settings for user preference
- const settings = await this.$accountSettings();
- const nativeNotificationTime = settings.nativeNotificationTime || "";
- this.nativeNotificationEnabled = !!nativeNotificationTime;
- this.nativeNotificationTimeStorage = nativeNotificationTime;
-
- if (nativeNotificationTime) {
- this.nativeNotificationTime = formatTimeForDisplay(
- nativeNotificationTime,
- );
- }
+ // No plugin schedule - UI defaults to disabled
+ this.nativeNotificationEnabled = false;
+ this.nativeNotificationTimeStorage = "";
+ this.nativeNotificationTime = "";
}
} catch (error) {
logger.error("[DailyNotificationSection] Failed to initialize:", error);
@@ -452,16 +568,14 @@ export default class DailyNotificationSection extends Vue {
priority: "high",
});
- // Save to settings
- await this.$saveSettings({
- nativeNotificationTime: this.nativeNotificationTimeStorage,
- nativeNotificationTitle: this.nativeNotificationTitle,
- nativeNotificationMessage: this.nativeNotificationMessage,
- });
-
// Update UI state
this.nativeNotificationEnabled = true;
+ // Refresh native fetcher configuration with fresh token
+ // This ensures plugin has valid authentication when notifications are first enabled
+ // Token will be valid for 72 hours, supporting offline prefetch operations
+ await this.refreshNativeFetcherConfig();
+
this.notify.success(
"Daily notification scheduled successfully",
TIMEOUTS.SHORT,
@@ -492,13 +606,6 @@ export default class DailyNotificationSection extends Vue {
// Cancel notification via PlatformService
await platformService.cancelDailyNotification();
- // Clear settings
- await this.$saveSettings({
- nativeNotificationTime: "",
- nativeNotificationTitle: "",
- nativeNotificationMessage: "",
- });
-
// Update UI state
this.nativeNotificationEnabled = false;
this.nativeNotificationTime = "";
@@ -558,10 +665,7 @@ export default class DailyNotificationSection extends Vue {
if (this.nativeNotificationEnabled) {
await this.updateNotificationTime(this.nativeNotificationTimeStorage);
} else {
- // Just save the time preference
- await this.$saveSettings({
- nativeNotificationTime: this.nativeNotificationTimeStorage,
- });
+ // Just update local state (time preference stored in component)
this.showTimeEdit = false;
this.notify.success("Notification time saved", TIMEOUTS.SHORT);
}
@@ -574,12 +678,9 @@ export default class DailyNotificationSection extends Vue {
async updateNotificationTime(newTime: string): Promise {
// newTime is in "HH:mm" format from HTML5 time input
if (!this.nativeNotificationEnabled) {
- // If notification is disabled, just save the time preference
+ // If notification is disabled, just update local state
this.nativeNotificationTimeStorage = newTime;
this.nativeNotificationTime = formatTimeForDisplay(newTime);
- await this.$saveSettings({
- nativeNotificationTime: newTime,
- });
this.showTimeEdit = false;
return;
}
@@ -605,11 +706,6 @@ export default class DailyNotificationSection extends Vue {
this.nativeNotificationTimeStorage = newTime;
this.nativeNotificationTime = formatTimeForDisplay(newTime);
- // 4. Save to settings
- await this.$saveSettings({
- nativeNotificationTime: newTime,
- });
-
this.notify.success(
"Notification time updated successfully",
TIMEOUTS.SHORT,
diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts
index 7db68437..4a177786 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -199,14 +199,6 @@ const MIGRATIONS = [
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
`,
},
- {
- name: "006_add_nativeNotificationSettings_to_settings",
- sql: `
- ALTER TABLE settings ADD COLUMN nativeNotificationTime TEXT;
- ALTER TABLE settings ADD COLUMN nativeNotificationTitle TEXT;
- ALTER TABLE settings ADD COLUMN nativeNotificationMessage TEXT;
- `,
- },
];
/**
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index 0af24058..493e4596 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -53,11 +53,6 @@ export type Settings = {
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
- // Native notification settings (Capacitor only)
- nativeNotificationTime?: string; // "09:00" format (24-hour) - scheduled time for daily notification
- nativeNotificationTitle?: string; // Default: "Daily Update" - notification title
- nativeNotificationMessage?: string; // Default message - notification body text
-
partnerApiServer?: string; // partner server API URL
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts
index b8ff2d57..ac17c392 100644
--- a/src/libs/crypto/index.ts
+++ b/src/libs/crypto/index.ts
@@ -104,6 +104,71 @@ export const accessToken = async (did?: string) => {
}
};
+/**
+ * Generate a longer-lived access token for background operations
+ *
+ * This function creates JWT tokens with extended validity (default 72 hours) for use
+ * in background prefetch operations. The longer expiration period allows the daily
+ * notification plugin to work offline for extended periods without requiring the app
+ * to be in the foreground to refresh tokens.
+ *
+ * **Token Refresh Strategy (Hybrid Approach):**
+ * - Tokens are valid for 72 hours (configurable)
+ * - Tokens are refreshed proactively when:
+ * - App comes to foreground (via Capacitor 'resume' event)
+ * - Component mounts (DailyNotificationSection)
+ * - Notifications are enabled
+ * - If token expires while offline, plugin uses cached content
+ * - Next time app opens, token is automatically refreshed
+ *
+ * **Why 72 Hours?**
+ * - Balances security (read-only prefetch operations) with offline capability
+ * - Reduces need for app to wake itself for token refresh
+ * - Allows plugin to work offline for extended periods (e.g., weekend trips)
+ * - Longer than typical prefetch windows (5 minutes before notification)
+ *
+ * **Security Considerations:**
+ * - Tokens are used only for read-only prefetch operations
+ * - Tokens are stored securely in plugin's Room database
+ * - Tokens are refreshed proactively to minimize exposure window
+ * - No private keys are exposed to native code
+ *
+ * @param {string} did - User DID (Decentralized Identifier) for token issuer
+ * @param {number} expirationMinutes - Optional expiration in minutes (defaults to 72 hours = 4320 minutes)
+ * @return {Promise} JWT token with extended validity, or empty string if no DID provided
+ *
+ * @example
+ * ```typescript
+ * // Generate token with default 72-hour expiration
+ * const token = await accessTokenForBackground("did:ethr:0x...");
+ *
+ * // Generate token with custom expiration (24 hours)
+ * const token24h = await accessTokenForBackground("did:ethr:0x...", 24 * 60);
+ * ```
+ *
+ * @see {@link accessToken} For short-lived tokens (1 minute) for regular API requests
+ * @see {@link createEndorserJwtForDid} For JWT creation implementation
+ */
+export const accessTokenForBackground = async (
+ did?: string,
+ expirationMinutes?: number,
+): Promise => {
+ if (!did) {
+ return "";
+ }
+
+ // Use provided expiration or default to 72 hours (4320 minutes)
+ // This allows background prefetch operations to work offline for extended periods
+ const expirationSeconds = expirationMinutes
+ ? expirationMinutes * 60
+ : 72 * 60 * 60; // Default 72 hours
+
+ const nowEpoch = Math.floor(Date.now() / 1000);
+ const endEpoch = nowEpoch + expirationSeconds;
+ const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
+ return createEndorserJwtForDid(did, tokenPayload);
+};
+
/**
* Extract JWT from various URL formats
* @param jwtUrlText The URL containing the JWT
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 30b8d8b4..6ff9b503 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -1543,6 +1543,51 @@ export class CapacitorPlatformService
/**
* Configure native fetcher for background operations
+ *
+ * This method configures the daily notification plugin's native content fetcher
+ * with authentication credentials for background prefetch operations. It automatically
+ * retrieves the active DID from the database and generates a fresh JWT token with
+ * 72-hour expiration.
+ *
+ * **Authentication Flow:**
+ * 1. Retrieves active DID from `active_identity` table (single source of truth)
+ * 2. Generates JWT token with 72-hour expiration using `accessTokenForBackground()`
+ * 3. Configures plugin with API server URL, active DID, and JWT token
+ * 4. Plugin stores token in its Room database for background workers
+ *
+ * **Token Management:**
+ * - Tokens are valid for 72 hours (4320 minutes)
+ * - Tokens are refreshed proactively when app comes to foreground
+ * - If token expires while offline, plugin uses cached content
+ * - Token refresh happens automatically via `DailyNotificationSection.refreshNativeFetcherConfig()`
+ *
+ * **Offline-First Design:**
+ * - 72-hour validity supports extended offline periods
+ * - Plugin can prefetch content when online and use cached content when offline
+ * - No app wake-up required for token refresh (happens when app is already open)
+ *
+ * **Error Handling:**
+ * - Returns `null` if active DID not found (no user logged in)
+ * - Returns `null` if JWT generation fails
+ * - Logs errors but doesn't throw (allows graceful degradation)
+ *
+ * @param config - Native fetcher configuration
+ * @param config.apiServer - API server URL (optional, uses default if not provided)
+ * @param config.jwt - JWT token (ignored, generated automatically)
+ * @param config.starredPlanHandleIds - Array of starred plan handle IDs for prefetch
+ * @returns Promise that resolves when configured, or `null` if configuration failed
+ *
+ * @example
+ * ```typescript
+ * await platformService.configureNativeFetcher({
+ * apiServer: "https://api.endorser.ch",
+ * jwt: "", // Generated automatically
+ * starredPlanHandleIds: ["plan-123", "plan-456"]
+ * });
+ * ```
+ *
+ * @see {@link accessTokenForBackground} For JWT token generation
+ * @see {@link DailyNotificationSection.refreshNativeFetcherConfig} For proactive token refresh
* @see PlatformService.configureNativeFetcher
*/
async configureNativeFetcher(
@@ -1553,16 +1598,57 @@ export class CapacitorPlatformService
"@timesafari/daily-notification-plugin"
);
- // Plugin expects apiBaseUrl, activeDid, and jwtToken
- // We'll need to get activeDid from somewhere - for now pass empty string
- // Components should provide activeDid when calling this
+ // Step 1: Get activeDid from database (single source of truth)
+ // This ensures we're using the correct user identity for authentication
+ const activeIdentity = await this.getActiveIdentity();
+ const activeDid = activeIdentity.activeDid;
+
+ if (!activeDid) {
+ logger.warn(
+ "[CapacitorPlatformService] No activeDid found, cannot configure native fetcher",
+ );
+ return null;
+ }
+
+ // Step 2: Generate JWT token for background operations
+ // Use 72-hour expiration for offline-first prefetch operations
+ // This allows the plugin to work offline for extended periods
+ const { accessTokenForBackground } = await import(
+ "../../libs/crypto/index"
+ );
+ // Use 72 hours (4320 minutes) for background prefetch tokens
+ // This is longer than passkey expiration to support offline scenarios
+ const expirationMinutes = 72 * 60; // 72 hours
+ const jwtToken = await accessTokenForBackground(
+ activeDid,
+ expirationMinutes,
+ );
+
+ if (!jwtToken) {
+ logger.error("[CapacitorPlatformService] Failed to generate JWT token");
+ return null;
+ }
+
+ // Step 3: Get API server from config or use default
+ // This ensures the plugin knows where to fetch content from
+ const apiServer =
+ config.apiServer ||
+ (await import("../../constants/app")).DEFAULT_ENDORSER_API_SERVER;
+
+ // Step 4: Configure plugin with credentials
+ // Plugin stores these in its Room database for background workers
await DailyNotification.configureNativeFetcher({
- apiBaseUrl: config.apiServer,
- activeDid: "", // Should be provided by caller
- jwtToken: config.jwt,
+ apiBaseUrl: apiServer,
+ activeDid,
+ jwtToken,
});
- logger.info("[CapacitorPlatformService] Configured native fetcher");
+ logger.info("[CapacitorPlatformService] Configured native fetcher", {
+ activeDid,
+ apiServer,
+ tokenExpirationHours: 72,
+ tokenExpirationMinutes: expirationMinutes,
+ });
} catch (error) {
logger.error(
"[CapacitorPlatformService] Failed to configure native fetcher:",
diff --git a/vite.config.capacitor.mts b/vite.config.capacitor.mts
index b47e5abe..e3bfd9f1 100644
--- a/vite.config.capacitor.mts
+++ b/vite.config.capacitor.mts
@@ -1,4 +1,23 @@
import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
-export default defineConfig(async () => createBuildConfig('capacitor'));
\ No newline at end of file
+export default defineConfig(async () => {
+ const baseConfig = await createBuildConfig('capacitor');
+
+ return {
+ ...baseConfig,
+ build: {
+ ...baseConfig.build,
+ rollupOptions: {
+ ...baseConfig.build?.rollupOptions,
+ // Externalize Capacitor plugins that are bundled natively
+ external: [
+ "@timesafari/daily-notification-plugin"
+ ],
+ output: {
+ ...baseConfig.build?.rollupOptions?.output,
+ }
+ }
+ }
+ };
+});
\ No newline at end of file
--
2.30.2
From 816c7a65824947b00e3d597dcde1fbac4a089aa1 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Mon, 10 Nov 2025 04:23:45 +0000
Subject: [PATCH 12/17] Fix daily notification plugin integration and bundling
- Change from dynamic to static imports for @timesafari/daily-notification-plugin
- Remove plugin from external dependencies in vite.config.capacitor.mts to ensure proper bundling
- Add debug logging to DailyNotificationSection for troubleshooting
- Update package-lock.json with latest dependencies
---
package-lock.json | 2 +-
.../DailyNotificationSection.vue | 11 +++++-
.../platforms/CapacitorPlatformService.ts | 36 +++++--------------
vite.config.capacitor.mts | 6 ++--
4 files changed, 22 insertions(+), 33 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 93b98785..981ff658 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -152,7 +152,7 @@
},
"../daily-notification-plugin": {
"name": "@timesafari/daily-notification-plugin",
- "version": "1.0.1",
+ "version": "1.0.3",
"license": "MIT",
"workspaces": [
"packages/*"
diff --git a/src/components/notifications/DailyNotificationSection.vue b/src/components/notifications/DailyNotificationSection.vue
index 9858014d..39f84962 100644
--- a/src/components/notifications/DailyNotificationSection.vue
+++ b/src/components/notifications/DailyNotificationSection.vue
@@ -357,6 +357,10 @@ export default class DailyNotificationSection extends Vue {
this.loading = true;
const platformService = PlatformServiceFactory.getInstance();
+ logger.debug(
+ "[DailyNotificationSection] Checking notification support...",
+ );
+
// Check if notifications are supported on this platform
// This also verifies plugin availability (returns null if plugin unavailable)
const status = await platformService.getDailyNotificationStatus();
@@ -364,11 +368,16 @@ export default class DailyNotificationSection extends Vue {
// Notifications not supported or plugin unavailable - don't initialize
this.notificationsSupported = false;
logger.warn(
- "[DailyNotificationSection] Notifications not supported or plugin unavailable",
+ "[DailyNotificationSection] Notifications not supported or plugin unavailable - section will be hidden",
);
return;
}
+ logger.debug(
+ "[DailyNotificationSection] Notifications supported, status:",
+ status,
+ );
+
this.notificationsSupported = true;
this.notificationStatus = status;
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 4b49d123..97a8a315 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -13,6 +13,7 @@ import {
CapacitorSQLite,
DBSQLiteValues,
} from "@capacitor-community/sqlite";
+import { DailyNotification } from "@timesafari/daily-notification-plugin";
import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database";
@@ -1421,9 +1422,8 @@ export class CapacitorPlatformService
*/
async getDailyNotificationStatus(): Promise {
try {
- // Dynamic import to avoid build issues if plugin unavailable
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
+ logger.debug(
+ "[CapacitorPlatformService] Getting daily notification status...",
);
const pluginStatus = await DailyNotification.getNotificationStatus();
@@ -1463,10 +1463,16 @@ export class CapacitorPlatformService
},
};
} catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
logger.error(
"[CapacitorPlatformService] Failed to get notification status:",
+ errorMessage,
error,
);
+ logger.warn(
+ "[CapacitorPlatformService] Daily notification section will be hidden - plugin may not be installed or available",
+ );
return null;
}
}
@@ -1477,10 +1483,6 @@ export class CapacitorPlatformService
*/
async checkNotificationPermissions(): Promise {
try {
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
- );
-
const permissions = await DailyNotification.checkPermissions();
// Log the raw permission state for debugging
@@ -1530,10 +1532,6 @@ export class CapacitorPlatformService
*/
async requestNotificationPermissions(): Promise {
try {
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
- );
-
logger.info(
`[CapacitorPlatformService] Requesting notification permissions...`,
);
@@ -1571,10 +1569,6 @@ export class CapacitorPlatformService
*/
async scheduleDailyNotification(options: ScheduleOptions): Promise {
try {
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
- );
-
await DailyNotification.scheduleDailyNotification({
time: options.time,
title: options.title,
@@ -1601,10 +1595,6 @@ export class CapacitorPlatformService
*/
async cancelDailyNotification(): Promise {
try {
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
- );
-
await DailyNotification.cancelAllNotifications();
logger.info("[CapacitorPlatformService] Cancelled daily notification");
@@ -1670,10 +1660,6 @@ export class CapacitorPlatformService
config: NativeFetcherConfig,
): Promise {
try {
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
- );
-
// Step 1: Get activeDid from database (single source of truth)
// This ensures we're using the correct user identity for authentication
const activeIdentity = await this.getActiveIdentity();
@@ -1740,10 +1726,6 @@ export class CapacitorPlatformService
*/
async updateStarredPlans(plans: { planIds: string[] }): Promise {
try {
- const { DailyNotification } = await import(
- "@timesafari/daily-notification-plugin"
- );
-
await DailyNotification.updateStarredPlans({
planIds: plans.planIds,
});
diff --git a/vite.config.capacitor.mts b/vite.config.capacitor.mts
index e3bfd9f1..27a722a4 100644
--- a/vite.config.capacitor.mts
+++ b/vite.config.capacitor.mts
@@ -10,10 +10,8 @@ export default defineConfig(async () => {
...baseConfig.build,
rollupOptions: {
...baseConfig.build?.rollupOptions,
- // Externalize Capacitor plugins that are bundled natively
- external: [
- "@timesafari/daily-notification-plugin"
- ],
+ // Note: @timesafari/daily-notification-plugin is NOT externalized
+ // because it needs to be bundled for dynamic imports to work in Capacitor WebView
output: {
...baseConfig.build?.rollupOptions?.output,
}
--
2.30.2
From 80d519925921dd9f488f7c28ca45253c7fc4ae79 Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Tue, 11 Nov 2025 01:04:20 +0000
Subject: [PATCH 13/17] chore: synchronize package
---
package-lock.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package-lock.json b/package-lock.json
index 981ff658..be618479 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -152,7 +152,7 @@
},
"../daily-notification-plugin": {
"name": "@timesafari/daily-notification-plugin",
- "version": "1.0.3",
+ "version": "1.0.8",
"license": "MIT",
"workspaces": [
"packages/*"
--
2.30.2
From b585c4d18374bbebc6ba507a689a8e7774fe46bc Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Tue, 11 Nov 2025 05:03:25 +0000
Subject: [PATCH 14/17] feat(android): Add host-side setup for daily
notification plugin
Implement Android host-side integration for daily notification plugin
by creating custom Application class and native content fetcher.
Changes:
- Add TimeSafariApplication.java: Custom Application class that registers
NativeNotificationContentFetcher with the plugin on app startup
- Add TimeSafariNativeFetcher.java: Implementation of NativeNotificationContentFetcher
interface that fetches notification content from endorser API endpoint
(/api/v2/report/plansLastUpdatedBetween) using JWT authentication
- Update AndroidManifest.xml: Declare TimeSafariApplication as the custom
Application class using android:name attribute
- Add Gson dependency: Include com.google.code.gson:gson:2.10.1 in build.gradle
for JSON parsing in the native fetcher
This setup mirrors the test app configuration and enables the plugin's
background content prefetching feature. The native fetcher will be called
by the plugin 5 minutes before scheduled notification times to prefetch
content for display.
Author: Matthew Raymer
---
android/app/build.gradle | 2 +
android/app/src/main/AndroidManifest.xml | 1 +
.../app/timesafari/TimeSafariApplication.java | 41 ++
.../timesafari/TimeSafariNativeFetcher.java | 549 ++++++++++++++++++
4 files changed, 593 insertions(+)
create mode 100644 android/app/src/main/java/app/timesafari/TimeSafariApplication.java
create mode 100644 android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java
diff --git a/android/app/build.gradle b/android/app/build.gradle
index edd4acd1..84ef47ae 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -101,6 +101,8 @@ dependencies {
implementation project(':capacitor-android')
implementation project(':capacitor-community-sqlite')
implementation "androidx.biometric:biometric:1.2.0-alpha05"
+ // Gson for JSON parsing in native notification fetcher
+ implementation "com.google.code.gson:gson:2.10.1"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6392c86b..13c10268 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
Architecture Note: The JWT token is pre-generated in TypeScript using
+ * TimeSafari's {@code accessTokenForBackground()} function (ES256K DID-based signing).
+ * The native fetcher just uses the token directly - no JWT generation needed.
+ *
+ * @param apiBaseUrl Base URL for API server (e.g., "https://api.endorser.ch")
+ * @param activeDid Active DID for authentication (e.g., "did:ethr:0x...")
+ * @param jwtToken Pre-generated JWT token (ES256K signed) from TypeScript
+ */
+ @Override
+ public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
+ this.apiBaseUrl = apiBaseUrl;
+ this.activeDid = activeDid;
+ this.jwtToken = jwtToken;
+
+ // Enhanced logging for JWT diagnostic purposes
+ Log.i(TAG, "TimeSafariNativeFetcher: Configured with API: " + apiBaseUrl);
+ if (activeDid != null) {
+ Log.i(TAG, "TimeSafariNativeFetcher: ActiveDID: " + activeDid.substring(0, Math.min(30, activeDid.length())) +
+ (activeDid.length() > 30 ? "..." : ""));
+ } else {
+ Log.w(TAG, "TimeSafariNativeFetcher: ActiveDID is NULL");
+ }
+
+ if (jwtToken != null) {
+ Log.i(TAG, "TimeSafariNativeFetcher: JWT token received - Length: " + jwtToken.length() + " chars");
+ // Log first and last 10 chars for verification (not full token for security)
+ String tokenPreview = jwtToken.length() > 20
+ ? jwtToken.substring(0, 10) + "..." + jwtToken.substring(jwtToken.length() - 10)
+ : jwtToken.substring(0, Math.min(jwtToken.length(), 20)) + "...";
+ Log.d(TAG, "TimeSafariNativeFetcher: JWT preview: " + tokenPreview);
+ } else {
+ Log.e(TAG, "TimeSafariNativeFetcher: JWT token is NULL - API calls will fail");
+ }
+ }
+
+ @Override
+ @NonNull
+ public CompletableFuture> fetchContent(
+ @NonNull FetchContext context) {
+
+ Log.d(TAG, "TimeSafariNativeFetcher: Fetch triggered - trigger=" + context.trigger +
+ ", scheduledTime=" + context.scheduledTime + ", fetchTime=" + context.fetchTime);
+
+ // Start with retry count 0
+ return fetchContentWithRetry(context, 0);
+ }
+
+ /**
+ * Fetch content with retry logic for transient errors
+ *
+ * @param context Fetch context
+ * @param retryCount Current retry attempt (0 for first attempt)
+ * @return Future with notification contents or empty list on failure
+ */
+ private CompletableFuture> fetchContentWithRetry(
+ @NonNull FetchContext context, int retryCount) {
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // Check if configured
+ if (apiBaseUrl == null || activeDid == null || jwtToken == null) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Not configured. Call configureNativeFetcher() from TypeScript first.");
+ return Collections.emptyList();
+ }
+
+ Log.i(TAG, "TimeSafariNativeFetcher: Starting fetch from " + apiBaseUrl + ENDORSER_ENDPOINT);
+
+ // Build request URL
+ String urlString = apiBaseUrl + ENDORSER_ENDPOINT;
+ URL url = new URL(urlString);
+
+ // Create HTTP connection
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
+ connection.setReadTimeout(READ_TIMEOUT_MS);
+ connection.setRequestMethod("POST");
+ connection.setRequestProperty("Content-Type", "application/json");
+
+ // Diagnostic logging for JWT usage
+ if (jwtToken != null) {
+ String jwtPreview = jwtToken.length() > 20
+ ? jwtToken.substring(0, 10) + "..." + jwtToken.substring(jwtToken.length() - 10)
+ : jwtToken;
+ Log.d(TAG, "TimeSafariNativeFetcher: Using JWT for API call - Length: " + jwtToken.length() +
+ ", Preview: " + jwtPreview + ", ActiveDID: " +
+ (activeDid != null ? activeDid.substring(0, Math.min(30, activeDid.length())) + "..." : "null"));
+ } else {
+ Log.e(TAG, "TimeSafariNativeFetcher: JWT token is NULL when making API call!");
+ }
+
+ connection.setRequestProperty("Authorization", "Bearer " + jwtToken);
+ connection.setDoOutput(true);
+
+ // Build request body
+ Map requestBody = new HashMap<>();
+ requestBody.put("planIds", getStarredPlanIds());
+
+ // afterId is required by the API endpoint
+ // Use "0" for first request (no previous data), or stored jwtId for subsequent requests
+ String afterId = getLastAcknowledgedJwtId();
+ if (afterId == null || afterId.isEmpty()) {
+ afterId = "0"; // First request - start from beginning
+ }
+ requestBody.put("afterId", afterId);
+
+ String jsonBody = gson.toJson(requestBody);
+ Log.d(TAG, "TimeSafariNativeFetcher: Request body: " + jsonBody);
+
+ // Write request body
+ try (OutputStream os = connection.getOutputStream()) {
+ byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
+ os.write(input, 0, input.length);
+ }
+
+ // Execute request
+ int responseCode = connection.getResponseCode();
+ Log.d(TAG, "TimeSafariNativeFetcher: HTTP response code: " + responseCode);
+
+ if (responseCode == 200) {
+ // Read response
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+ reader.close();
+
+ String responseBody = response.toString();
+ Log.d(TAG, "TimeSafariNativeFetcher: Response body length: " + responseBody.length());
+
+ // Parse response and convert to NotificationContent
+ List contents = parseApiResponse(responseBody, context);
+
+ // Update last acknowledged JWT ID from the response (for pagination)
+ if (!contents.isEmpty()) {
+ // Get the last JWT ID from the parsed response (stored during parsing)
+ updateLastAckedJwtIdFromResponse(contents, responseBody);
+ }
+
+ Log.i(TAG, "TimeSafariNativeFetcher: Successfully fetched " + contents.size() +
+ " notification(s)");
+
+ return contents;
+
+ } else {
+ // Read error response
+ String errorMessage = "Unknown error";
+ try {
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8));
+ StringBuilder errorResponse = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ errorResponse.append(line);
+ }
+ reader.close();
+ errorMessage = errorResponse.toString();
+ } catch (Exception e) {
+ Log.w(TAG, "TimeSafariNativeFetcher: Could not read error stream", e);
+ }
+
+ Log.e(TAG, "TimeSafariNativeFetcher: API error " + responseCode + ": " + errorMessage);
+
+ // Handle retryable errors (5xx server errors, network timeouts)
+ if (shouldRetry(responseCode, retryCount)) {
+ long delayMs = RETRY_DELAY_MS * (1 << retryCount); // Exponential backoff
+ Log.w(TAG, "TimeSafariNativeFetcher: Retryable error, retrying in " + delayMs + "ms " +
+ "(" + (retryCount + 1) + "/" + MAX_RETRIES + ")");
+
+ try {
+ Thread.sleep(delayMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.e(TAG, "TimeSafariNativeFetcher: Retry delay interrupted", e);
+ return Collections.emptyList();
+ }
+
+ // Recursive retry
+ return fetchContentWithRetry(context, retryCount + 1).join();
+ }
+
+ // Non-retryable errors (4xx client errors, max retries reached)
+ if (responseCode >= 400 && responseCode < 500) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Non-retryable client error " + responseCode);
+ } else if (retryCount >= MAX_RETRIES) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Max retries (" + MAX_RETRIES + ") reached");
+ }
+
+ // Return empty list on error (fallback will be handled by worker)
+ return Collections.emptyList();
+ }
+
+ } catch (java.net.SocketTimeoutException | java.net.UnknownHostException e) {
+ // Network errors are retryable
+ Log.w(TAG, "TimeSafariNativeFetcher: Network error during fetch", e);
+
+ if (shouldRetry(0, retryCount)) { // Use 0 as response code for network errors
+ long delayMs = RETRY_DELAY_MS * (1 << retryCount);
+ Log.w(TAG, "TimeSafariNativeFetcher: Retrying after network error in " + delayMs + "ms " +
+ "(" + (retryCount + 1) + "/" + MAX_RETRIES + ")");
+
+ try {
+ Thread.sleep(delayMs);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ Log.e(TAG, "TimeSafariNativeFetcher: Retry delay interrupted", ie);
+ return Collections.emptyList();
+ }
+
+ return fetchContentWithRetry(context, retryCount + 1).join();
+ }
+
+ Log.e(TAG, "TimeSafariNativeFetcher: Max retries reached for network error");
+ return Collections.emptyList();
+
+ } catch (Exception e) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Error during fetch", e);
+ // Non-retryable errors (parsing, configuration, etc.)
+ return Collections.emptyList();
+ }
+ });
+ }
+
+ /**
+ * Determine if an error should be retried
+ *
+ * @param responseCode HTTP response code (0 for network errors)
+ * @param retryCount Current retry attempt count
+ * @return true if error is retryable and retry count not exceeded
+ */
+ private boolean shouldRetry(int responseCode, int retryCount) {
+ if (retryCount >= MAX_RETRIES) {
+ return false; // Max retries exceeded
+ }
+
+ // Retry on network errors (responseCode 0) or server errors (5xx)
+ // Don't retry on client errors (4xx) as they indicate permanent issues
+ if (responseCode == 0) {
+ return true; // Network error (timeout, unknown host, etc.)
+ }
+
+ if (responseCode >= 500 && responseCode < 600) {
+ return true; // Server error (retryable)
+ }
+
+ // Some 4xx errors might be retryable (e.g., 429 Too Many Requests)
+ if (responseCode == 429) {
+ return true; // Rate limit - retry with backoff
+ }
+
+ return false; // Other client errors (401, 403, 404, etc.) are not retryable
+ }
+
+ /**
+ * Get starred plan IDs from SharedPreferences
+ *
+ * @return List of starred plan IDs, empty list if none stored
+ */
+ private List getStarredPlanIds() {
+ try {
+ // Use the same SharedPreferences as the plugin (not the instance variable 'prefs')
+ // Plugin stores in "daily_notification_timesafari" with key "starredPlanIds"
+ SharedPreferences pluginPrefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ String idsJson = pluginPrefs.getString(KEY_STARRED_PLAN_IDS, "[]");
+
+ if (idsJson == null || idsJson.isEmpty() || idsJson.equals("[]")) {
+ Log.d(TAG, "TimeSafariNativeFetcher: No starred plan IDs found in SharedPreferences");
+ return new ArrayList<>();
+ }
+
+ // Parse JSON array (plugin stores as JSON string)
+ JsonParser parser = new JsonParser();
+ JsonArray jsonArray = parser.parse(idsJson).getAsJsonArray();
+ List planIds = new ArrayList<>();
+
+ for (int i = 0; i < jsonArray.size(); i++) {
+ planIds.add(jsonArray.get(i).getAsString());
+ }
+
+ Log.i(TAG, "TimeSafariNativeFetcher: Loaded " + planIds.size() + " starred plan IDs from SharedPreferences");
+ if (planIds.size() > 0) {
+ Log.d(TAG, "TimeSafariNativeFetcher: First plan ID: " +
+ planIds.get(0).substring(0, Math.min(30, planIds.get(0).length())) + "...");
+ }
+ return planIds;
+
+ } catch (Exception e) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Error loading starred plan IDs from SharedPreferences", e);
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * Get last acknowledged JWT ID from SharedPreferences (for pagination)
+ *
+ * @return Last acknowledged JWT ID, or null if none stored
+ */
+ private String getLastAcknowledgedJwtId() {
+ try {
+ String jwtId = prefs.getString(KEY_LAST_ACKED_JWT_ID, null);
+ if (jwtId != null) {
+ Log.d(TAG, "TimeSafariNativeFetcher: Loaded last acknowledged JWT ID");
+ }
+ return jwtId;
+ } catch (Exception e) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Error loading last acknowledged JWT ID", e);
+ return null;
+ }
+ }
+
+ /**
+ * Update last acknowledged JWT ID from the API response
+ * Uses the last JWT ID from the data array for pagination
+ *
+ * @param contents Parsed notification contents (may contain JWT IDs)
+ * @param responseBody Original response body for parsing
+ */
+ private void updateLastAckedJwtIdFromResponse(List contents, String responseBody) {
+ try {
+ JsonParser parser = new JsonParser();
+ JsonObject root = parser.parse(responseBody).getAsJsonObject();
+ JsonArray dataArray = root.getAsJsonArray("data");
+
+ if (dataArray != null && dataArray.size() > 0) {
+ // Get the last item's JWT ID (most recent)
+ JsonObject lastItem = dataArray.get(dataArray.size() - 1).getAsJsonObject();
+
+ // Try to get JWT ID from different possible locations in response structure
+ String jwtId = null;
+ if (lastItem.has("jwtId")) {
+ jwtId = lastItem.get("jwtId").getAsString();
+ } else if (lastItem.has("plan")) {
+ JsonObject plan = lastItem.getAsJsonObject("plan");
+ if (plan.has("jwtId")) {
+ jwtId = plan.get("jwtId").getAsString();
+ }
+ }
+
+ if (jwtId != null && !jwtId.isEmpty()) {
+ updateLastAckedJwtId(jwtId);
+ Log.d(TAG, "TimeSafariNativeFetcher: Updated last acknowledged JWT ID: " +
+ jwtId.substring(0, Math.min(20, jwtId.length())) + "...");
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "TimeSafariNativeFetcher: Could not extract JWT ID from response for pagination", e);
+ }
+ }
+
+ /**
+ * Update last acknowledged JWT ID in SharedPreferences
+ *
+ * @param jwtId JWT ID to store as last acknowledged
+ */
+ private void updateLastAckedJwtId(String jwtId) {
+ try {
+ prefs.edit().putString(KEY_LAST_ACKED_JWT_ID, jwtId).apply();
+ Log.d(TAG, "TimeSafariNativeFetcher: Updated last acknowledged JWT ID");
+ } catch (Exception e) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Error updating last acknowledged JWT ID", e);
+ }
+ }
+
+ /**
+ * Parse API response and convert to NotificationContent list
+ */
+ private List parseApiResponse(String responseBody, FetchContext context) {
+ List contents = new ArrayList<>();
+
+ try {
+ JsonParser parser = new JsonParser();
+ JsonObject root = parser.parse(responseBody).getAsJsonObject();
+
+ // Parse response structure (matches PlansLastUpdatedResponse)
+ JsonArray dataArray = root.getAsJsonArray("data");
+ if (dataArray != null) {
+ for (int i = 0; i < dataArray.size(); i++) {
+ JsonObject item = dataArray.get(i).getAsJsonObject();
+
+ NotificationContent content = new NotificationContent();
+
+ // Extract data from API response
+ // Support both flat structure (jwtId, planId) and nested (plan.jwtId, plan.handleId)
+ String planId = null;
+ String jwtId = null;
+
+ if (item.has("planId")) {
+ planId = item.get("planId").getAsString();
+ } else if (item.has("plan")) {
+ JsonObject plan = item.getAsJsonObject("plan");
+ if (plan.has("handleId")) {
+ planId = plan.get("handleId").getAsString();
+ }
+ }
+
+ if (item.has("jwtId")) {
+ jwtId = item.get("jwtId").getAsString();
+ } else if (item.has("plan")) {
+ JsonObject plan = item.getAsJsonObject("plan");
+ if (plan.has("jwtId")) {
+ jwtId = plan.get("jwtId").getAsString();
+ }
+ }
+
+ // Create notification ID
+ String notificationId = "endorser_" + (jwtId != null ? jwtId :
+ System.currentTimeMillis() + "_" + i);
+ content.setId(notificationId);
+
+ // Create notification title
+ String title = "Project Update";
+ if (planId != null) {
+ title = "Update: " + planId.substring(Math.max(0, planId.length() - 8));
+ }
+ content.setTitle(title);
+
+ // Create notification body
+ StringBuilder body = new StringBuilder();
+ if (planId != null) {
+ body.append("Plan ").append(planId.substring(Math.max(0, planId.length() - 12))).append(" has been updated.");
+ } else {
+ body.append("A project you follow has been updated.");
+ }
+ content.setBody(body.toString());
+
+ // Use scheduled time from context, or default to 1 hour from now
+ long scheduledTimeMs = context.scheduledTime != null ?
+ context.scheduledTime : (System.currentTimeMillis() + 3600000);
+ content.setScheduledTime(scheduledTimeMs);
+
+ // Set notification properties
+ content.setPriority("default");
+ content.setSound(true);
+
+ contents.add(content);
+ }
+ }
+
+ // If no data items, create a default notification
+ if (contents.isEmpty()) {
+ NotificationContent defaultContent = new NotificationContent();
+ defaultContent.setId("endorser_no_updates_" + System.currentTimeMillis());
+ defaultContent.setTitle("No Project Updates");
+ defaultContent.setBody("No updates found in your starred projects.");
+
+ long scheduledTimeMs = context.scheduledTime != null ?
+ context.scheduledTime : (System.currentTimeMillis() + 3600000);
+ defaultContent.setScheduledTime(scheduledTimeMs);
+ defaultContent.setPriority("default");
+ defaultContent.setSound(true);
+
+ contents.add(defaultContent);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "TimeSafariNativeFetcher: Error parsing API response", e);
+ // Return empty list on parse error
+ }
+
+ return contents;
+ }
+}
+
--
2.30.2
From 28a825a46001ec8b44ed8344168301e3e2677c5d Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Tue, 11 Nov 2025 05:05:10 +0000
Subject: [PATCH 15/17] feat(android): Add instrumentation logs for prefetch
investigation
Add comprehensive instrumentation to diagnose why native fetcher
is not being called during prefetch operations.
Instrumentation Added:
- TimeSafariApplication: Log app initialization, fetcher registration,
and verification with process ID and timestamps
- TimeSafariNativeFetcher: Log configuration, fetch start, source
resolution (native vs fallback), and write completion
- Diagnostic script: Filter logcat for prefetch-related events
- Investigation summary: Document root cause hypotheses and
diagnostic checklist
Log Tags:
- APP|ON_CREATE: App initialization timing and process info
- FETCHER|REGISTER_START/REGISTERED: Fetcher registration lifecycle
- FETCHER|CONFIGURE_START/CONFIGURE_COMPLETE: Configuration tracking
- PREFETCH|START/SOURCE/WRITE_OK: Prefetch operation tracking
- DISPLAY|START/LOOKUP: Display worker tracking (future)
- STORAGE|POST_PREFETCH/PRE_DISPLAY: Storage verification (future)
This instrumentation will help diagnose:
1. Registration timing issues (worker before onCreate)
2. Fetcher resolution failures (null fetcher)
3. Process mismatches (multi-process issues)
4. ID schema inconsistencies (prefetch vs display)
5. Storage persistence issues (content not saved)
Author: Matthew Raymer
---
.../app/timesafari/TimeSafariApplication.java | 30 +++++
.../timesafari/TimeSafariNativeFetcher.java | 56 +++++++++
docs/prefetch-investigation-summary.md | 109 ++++++++++++++++++
scripts/diagnose-prefetch.sh | 36 ++++++
4 files changed, 231 insertions(+)
create mode 100644 docs/prefetch-investigation-summary.md
create mode 100755 scripts/diagnose-prefetch.sh
diff --git a/android/app/src/main/java/app/timesafari/TimeSafariApplication.java b/android/app/src/main/java/app/timesafari/TimeSafariApplication.java
index e1160502..8c14b3b0 100644
--- a/android/app/src/main/java/app/timesafari/TimeSafariApplication.java
+++ b/android/app/src/main/java/app/timesafari/TimeSafariApplication.java
@@ -27,14 +27,44 @@ public class TimeSafariApplication extends Application {
public void onCreate() {
super.onCreate();
+ // Instrumentation: Log app initialization with process info
+ int pid = android.os.Process.myPid();
+ String processName = getApplicationInfo().processName;
+ Log.i(TAG, String.format(
+ "APP|ON_CREATE ts=%d pid=%d processName=%s",
+ System.currentTimeMillis(),
+ pid,
+ processName
+ ));
+
Log.i(TAG, "Initializing TimeSafari Application");
// Register native fetcher with application context
Context context = getApplicationContext();
NativeNotificationContentFetcher nativeFetcher =
new TimeSafariNativeFetcher(context);
+
+ // Instrumentation: Log before registration
+ Log.i(TAG, String.format(
+ "FETCHER|REGISTER_START instanceHash=%d ts=%d",
+ nativeFetcher.hashCode(),
+ System.currentTimeMillis()
+ ));
+
DailyNotificationPlugin.setNativeFetcher(nativeFetcher);
+ // Instrumentation: Verify registration succeeded
+ NativeNotificationContentFetcher verified =
+ DailyNotificationPlugin.getNativeFetcherStatic();
+ boolean registered = (verified != null && verified == nativeFetcher);
+
+ Log.i(TAG, String.format(
+ "FETCHER|REGISTERED providerKey=DailyNotificationPlugin instanceHash=%d registered=%s ts=%d",
+ nativeFetcher.hashCode(),
+ registered,
+ System.currentTimeMillis()
+ ));
+
Log.i(TAG, "Native fetcher registered: " + nativeFetcher.getClass().getName());
}
}
diff --git a/android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java b/android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java
index 92928aba..2c631783 100644
--- a/android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java
+++ b/android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java
@@ -92,10 +92,31 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
*/
@Override
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
+ // Instrumentation: Log configuration start
+ int pid = android.os.Process.myPid();
+ Log.i(TAG, String.format(
+ "FETCHER|CONFIGURE_START instanceHash=%d pid=%d ts=%d",
+ this.hashCode(),
+ pid,
+ System.currentTimeMillis()
+ ));
+
this.apiBaseUrl = apiBaseUrl;
this.activeDid = activeDid;
this.jwtToken = jwtToken;
+ // Instrumentation: Log configuration completion
+ boolean configured = (apiBaseUrl != null && activeDid != null && jwtToken != null);
+ Log.i(TAG, String.format(
+ "FETCHER|CONFIGURE_COMPLETE instanceHash=%d configured=%s apiBaseUrl=%s activeDid=%s jwtLength=%d ts=%d",
+ this.hashCode(),
+ configured,
+ apiBaseUrl != null ? apiBaseUrl : "null",
+ activeDid != null ? activeDid.substring(0, Math.min(30, activeDid.length())) + "..." : "null",
+ jwtToken != null ? jwtToken.length() : 0,
+ System.currentTimeMillis()
+ ));
+
// Enhanced logging for JWT diagnostic purposes
Log.i(TAG, "TimeSafariNativeFetcher: Configured with API: " + apiBaseUrl);
if (activeDid != null) {
@@ -122,6 +143,18 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
public CompletableFuture> fetchContent(
@NonNull FetchContext context) {
+ // Instrumentation: Log fetch start with context
+ int pid = android.os.Process.myPid();
+ Log.i(TAG, String.format(
+ "PREFETCH|START id=%s notifyAt=%d trigger=%s instanceHash=%d pid=%d ts=%d",
+ context.scheduledTime != null ? "daily_" + context.scheduledTime : "unknown",
+ context.scheduledTime != null ? context.scheduledTime : 0,
+ context.trigger,
+ this.hashCode(),
+ pid,
+ System.currentTimeMillis()
+ ));
+
Log.d(TAG, "TimeSafariNativeFetcher: Fetch triggered - trigger=" + context.trigger +
", scheduledTime=" + context.scheduledTime + ", fetchTime=" + context.fetchTime);
@@ -143,10 +176,25 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
try {
// Check if configured
if (apiBaseUrl == null || activeDid == null || jwtToken == null) {
+ Log.e(TAG, String.format(
+ "PREFETCH|SOURCE from=fallback reason=not_configured apiBaseUrl=%s activeDid=%s jwtToken=%s ts=%d",
+ apiBaseUrl != null ? "set" : "null",
+ activeDid != null ? "set" : "null",
+ jwtToken != null ? "set" : "null",
+ System.currentTimeMillis()
+ ));
Log.e(TAG, "TimeSafariNativeFetcher: Not configured. Call configureNativeFetcher() from TypeScript first.");
return Collections.emptyList();
}
+ // Instrumentation: Log native fetcher usage
+ Log.i(TAG, String.format(
+ "PREFETCH|SOURCE from=native instanceHash=%d apiBaseUrl=%s ts=%d",
+ this.hashCode(),
+ apiBaseUrl,
+ System.currentTimeMillis()
+ ));
+
Log.i(TAG, "TimeSafariNativeFetcher: Starting fetch from " + apiBaseUrl + ENDORSER_ENDPOINT);
// Build request URL
@@ -226,6 +274,14 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
Log.i(TAG, "TimeSafariNativeFetcher: Successfully fetched " + contents.size() +
" notification(s)");
+ // Instrumentation: Log successful fetch
+ Log.i(TAG, String.format(
+ "PREFETCH|WRITE_OK id=%s items=%d ts=%d",
+ context.scheduledTime != null ? "daily_" + context.scheduledTime : "unknown",
+ contents.size(),
+ System.currentTimeMillis()
+ ));
+
return contents;
} else {
diff --git a/docs/prefetch-investigation-summary.md b/docs/prefetch-investigation-summary.md
new file mode 100644
index 00000000..ed360695
--- /dev/null
+++ b/docs/prefetch-investigation-summary.md
@@ -0,0 +1,109 @@
+# Prefetch Investigation Summary
+
+## Problem Statement
+
+The daily notification prefetch job (T-5 min) is not calling the native fetcher, resulting in:
+- `from: null` in prefetch logs
+- Fallback/mock content being used
+- `DISPLAY_SKIP content_not_found` at notification time
+- Storage empty (`[]`) when display worker runs
+
+## Root Cause Hypothesis
+
+Based on the directive analysis, likely causes (ranked):
+
+1. **Registration Timing**: Prefetch worker runs before `Application.onCreate()` completes
+2. **Discovery Failure**: Worker resolves fetcher to `null` (wrong scope, process mismatch)
+3. **Persistence Bug**: Content written but wiped/deduped before display
+4. **ID Mismatch**: Prefetch writes `notify_...` but display looks for `daily_...`
+
+## Instrumentation Added
+
+### TimeSafariApplication.java
+- `APP|ON_CREATE ts=... pid=... processName=...` - App initialization timing
+- `FETCHER|REGISTER_START instanceHash=... ts=...` - Before registration
+- `FETCHER|REGISTERED providerKey=... instanceHash=... registered=... ts=...` - After registration with verification
+
+### TimeSafariNativeFetcher.java
+- `FETCHER|CONFIGURE_START instanceHash=... pid=... ts=...` - Configuration start
+- `FETCHER|CONFIGURE_COMPLETE instanceHash=... configured=... apiBaseUrl=... activeDid=... jwtLength=... ts=...` - Configuration completion
+- `PREFETCH|START id=... notifyAt=... trigger=... instanceHash=... pid=... ts=...` - Fetch start
+- `PREFETCH|SOURCE from=native/fallback reason=... ts=...` - Source resolution
+- `PREFETCH|WRITE_OK id=... items=... ts=...` - Successful fetch
+
+## Diagnostic Tools
+
+### Log Filtering Script
+```bash
+./scripts/diagnose-prefetch.sh app.timesafari.app
+```
+
+Filters logcat for:
+- `APP|ON_CREATE`
+- `FETCHER|*`
+- `PREFETCH|*`
+- `DISPLAY|*`
+- `STORAGE|*`
+
+### Manual Filtering
+```bash
+adb logcat | grep -E "APP\|ON_CREATE|FETCHER\||PREFETCH\||DISPLAY\||STORAGE\|"
+```
+
+## Investigation Checklist
+
+### A. App/Plugin Initialization Order
+- [ ] Confirm `APP|ON_CREATE` appears before `PREFETCH|START`
+- [ ] Verify `FETCHER|REGISTERED registered=true`
+- [ ] Check for multiple `onCreate` invocations (process restarts)
+- [ ] Confirm single process (no `android:process` on workers)
+
+### B. Prefetch Worker Resolution
+- [ ] Check `PREFETCH|SOURCE from=native` (not `from=fallback`)
+- [ ] Verify `instanceHash` matches between registration and fetch
+- [ ] Compare `pid` values (should be same process)
+- [ ] Check `FETCHER|CONFIGURE_COMPLETE configured=true` before prefetch
+
+### C. Storage & Persistence
+- [ ] Verify `PREFETCH|WRITE_OK items>=1`
+- [ ] Check storage logs for content persistence
+- [ ] Compare prefetch ID vs display lookup ID (must match)
+
+### D. ID Schema Consistency
+- [ ] Prefetch ID format: `daily_` or `notify_`
+- [ ] Display lookup ID format: must match prefetch ID
+- [ ] Verify ID derivation rules are consistent
+
+## Next Steps
+
+1. **Run diagnostic script** during a notification cycle
+2. **Analyze logs** for timing issues and process mismatches
+3. **If fetcher is null**: Implement Fix #2 (Pass Fetcher Context With Work) or Fix #3 (Process-Safe DI)
+4. **If ID mismatch**: Normalize ID schema across prefetch and display
+5. **If storage issue**: Add transactional writes and read-after-write verification
+
+## Expected Log Flow (Success Case)
+
+```
+APP|ON_CREATE ts=... pid=... processName=app.timesafari.app
+FETCHER|REGISTER_START instanceHash=... ts=...
+FETCHER|REGISTERED providerKey=DailyNotificationPlugin instanceHash=... registered=true ts=...
+FETCHER|CONFIGURE_START instanceHash=... pid=... ts=...
+FETCHER|CONFIGURE_COMPLETE instanceHash=... configured=true ... ts=...
+PREFETCH|START id=daily_... notifyAt=... trigger=prefetch instanceHash=... pid=... ts=...
+PREFETCH|SOURCE from=native instanceHash=... apiBaseUrl=... ts=...
+PREFETCH|WRITE_OK id=daily_... items=1 ts=...
+STORAGE|POST_PREFETCH total=1 ids=[daily_...]
+DISPLAY|START id=daily_...
+STORAGE|PRE_DISPLAY total=1 ids=[daily_...]
+DISPLAY|LOOKUP result=hit id=daily_...
+```
+
+## Failure Indicators
+
+- `PREFETCH|SOURCE from=fallback` - Native fetcher not resolved
+- `PREFETCH|SOURCE from=null` - Fetcher registration failed
+- `FETCHER|REGISTERED registered=false` - Registration verification failed
+- `STORAGE|PRE_DISPLAY total=0` - Content not persisted
+- `DISPLAY|LOOKUP result=miss` - ID mismatch or content cleared
+
diff --git a/scripts/diagnose-prefetch.sh b/scripts/diagnose-prefetch.sh
new file mode 100755
index 00000000..59e52f6e
--- /dev/null
+++ b/scripts/diagnose-prefetch.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+#
+# Diagnostic script for daily notification prefetch issues
+# Filters logcat output for prefetch-related instrumentation logs
+#
+# Usage:
+# ./scripts/diagnose-prefetch.sh [package_name]
+#
+# Example:
+# ./scripts/diagnose-prefetch.sh app.timesafari.app
+#
+
+set -e
+
+PACKAGE_NAME="${1:-app.timesafari.app}"
+
+echo "🔍 Daily Notification Prefetch Diagnostic Tool"
+echo "=============================================="
+echo ""
+echo "Package: $PACKAGE_NAME"
+echo "Filtering for instrumentation tags:"
+echo " - APP|ON_CREATE"
+echo " - FETCHER|*"
+echo " - PREFETCH|*"
+echo " - DISPLAY|*"
+echo " - STORAGE|*"
+echo ""
+echo "Press Ctrl+C to stop"
+echo ""
+
+# Filter logcat for instrumentation tags
+adb logcat -c # Clear logcat buffer first
+
+adb logcat | grep -E "APP\|ON_CREATE|FETCHER\||PREFETCH\||DISPLAY\||STORAGE\||DailyNotification|TimeSafariApplication|TimeSafariNativeFetcher" | \
+ grep -i "$PACKAGE_NAME\|TimeSafari\|DailyNotification"
+
--
2.30.2
From 5528c44f2b0d1d52f17295cea340b78699a056be Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Tue, 11 Nov 2025 07:53:20 +0000
Subject: [PATCH 16/17] fix: Add notification channel creation and improve
native fetcher configuration
- Add notification channel creation in TimeSafariApplication for Android 8.0+
Required for daily notifications to display properly. Channel ID matches
plugin's 'timesafari.daily' channel.
- Convert localhost to 10.0.2.2 in CapacitorPlatformService for Android emulators
Android emulators cannot reach localhost - they need 10.0.2.2 to access
the host machine's API server.
- Refresh native fetcher configuration when API server changes in AccountViewView
Ensures background notification prefetch uses the updated endpoint when
user changes API server URL in settings.
- Add directive for fixing notification dismiss cancellation in plugin
Documents the fix needed in plugin source to cancel notification from
NotificationManager when dismiss button is clicked.
These changes ensure daily notifications work correctly on Android, including
proper channel setup, emulator network connectivity, and configuration refresh.
---
.../app/timesafari/TimeSafariApplication.java | 47 ++++++++
.../fix-notification-dismiss-cancel.mdc | 109 ++++++++++++++++++
.../platforms/CapacitorPlatformService.ts | 13 ++-
src/views/AccountViewView.vue | 28 +++++
4 files changed, 196 insertions(+), 1 deletion(-)
create mode 100644 docs/directives/fix-notification-dismiss-cancel.mdc
diff --git a/android/app/src/main/java/app/timesafari/TimeSafariApplication.java b/android/app/src/main/java/app/timesafari/TimeSafariApplication.java
index 8c14b3b0..feffc5c5 100644
--- a/android/app/src/main/java/app/timesafari/TimeSafariApplication.java
+++ b/android/app/src/main/java/app/timesafari/TimeSafariApplication.java
@@ -11,7 +11,10 @@
package app.timesafari;
import android.app.Application;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
import android.content.Context;
+import android.os.Build;
import android.util.Log;
import com.timesafari.dailynotification.DailyNotificationPlugin;
import com.timesafari.dailynotification.NativeNotificationContentFetcher;
@@ -39,6 +42,9 @@ public class TimeSafariApplication extends Application {
Log.i(TAG, "Initializing TimeSafari Application");
+ // Create notification channel for daily notifications (required for Android 8.0+)
+ createNotificationChannel();
+
// Register native fetcher with application context
Context context = getApplicationContext();
NativeNotificationContentFetcher nativeFetcher =
@@ -67,5 +73,46 @@ public class TimeSafariApplication extends Application {
Log.i(TAG, "Native fetcher registered: " + nativeFetcher.getClass().getName());
}
+
+ /**
+ * Create notification channel for daily notifications
+ * Required for Android 8.0 (API 26) and above
+ */
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationManager notificationManager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (notificationManager == null) {
+ Log.w(TAG, "NotificationManager is null, cannot create channel");
+ return;
+ }
+
+ // Channel ID must match the one used in DailyNotificationWorker
+ String channelId = "timesafari.daily";
+ String channelName = "Daily Notifications";
+ String channelDescription = "Daily notification updates from TimeSafari";
+
+ // Check if channel already exists
+ NotificationChannel existingChannel = notificationManager.getNotificationChannel(channelId);
+ if (existingChannel != null) {
+ Log.d(TAG, "Notification channel already exists: " + channelId);
+ return;
+ }
+
+ // Create the channel with high importance (for priority="high" notifications)
+ NotificationChannel channel = new NotificationChannel(
+ channelId,
+ channelName,
+ NotificationManager.IMPORTANCE_HIGH
+ );
+ channel.setDescription(channelDescription);
+ channel.enableVibration(true);
+ channel.setShowBadge(true);
+
+ notificationManager.createNotificationChannel(channel);
+ Log.i(TAG, "Notification channel created: " + channelId);
+ }
+ }
}
diff --git a/docs/directives/fix-notification-dismiss-cancel.mdc b/docs/directives/fix-notification-dismiss-cancel.mdc
new file mode 100644
index 00000000..fbe55776
--- /dev/null
+++ b/docs/directives/fix-notification-dismiss-cancel.mdc
@@ -0,0 +1,109 @@
+# Fix Notification Dismiss to Cancel Notification
+
+## Problem
+
+When a user clicks the "Dismiss" button on a daily notification, the notification is removed from storage and alarms are cancelled, but the notification itself is not cancelled from the NotificationManager. This means the notification remains visible in the system tray even though it's been dismissed.
+
+Additionally, clicking on the notification (not the dismiss button) launches the app, which is working as intended.
+
+## Root Cause
+
+In `DailyNotificationWorker.java`, the `handleDismissNotification()` method:
+1. ✅ Removes notification from storage
+2. ✅ Cancels pending alarms
+3. ❌ **MISSING**: Does not cancel the notification from NotificationManager
+
+The notification is displayed with ID = `content.getId().hashCode()` (line 440), but this ID is never used to cancel the notification when dismissing.
+
+## Solution
+
+Add notification cancellation to `handleDismissNotification()` method in `DailyNotificationWorker.java`.
+
+### IMPORTANT: Plugin Source Change
+
+**This change must be applied to the plugin source repository**, not the host app. The file is located in the `@timesafari/daily-notification-plugin` package.
+
+### File to Modify
+
+**Plugin Source Repository:**
+`android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
+
+**Note:** In the host app's `node_modules`, this file is located at:
+`node_modules/@timesafari/daily-notification-plugin/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
+
+However, changes to `node_modules` will be overwritten on the next `npm install`. This fix must be applied to the plugin's source repository.
+
+### Change Required
+
+In the `handleDismissNotification()` method (around line 177-206), add code to cancel the notification from NotificationManager:
+
+```java
+private Result handleDismissNotification(String notificationId) {
+ Trace.beginSection("DN:dismiss");
+ try {
+ Log.d(TAG, "DN|DISMISS_START id=" + notificationId);
+
+ // Cancel the notification from NotificationManager FIRST
+ // This ensures the notification disappears immediately when dismissed
+ NotificationManager notificationManager =
+ (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager != null) {
+ int systemNotificationId = notificationId.hashCode();
+ notificationManager.cancel(systemNotificationId);
+ Log.d(TAG, "DN|DISMISS_CANCEL_NOTIF systemId=" + systemNotificationId);
+ }
+
+ // Remove from Room if present; also remove from legacy storage for compatibility
+ try {
+ DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext());
+ // No direct delete DAO exposed via service; legacy removal still applied
+ } catch (Exception ignored) { }
+ DailyNotificationStorage storage = new DailyNotificationStorage(getApplicationContext());
+ storage.removeNotification(notificationId);
+
+ // Cancel any pending alarms
+ DailyNotificationScheduler scheduler = new DailyNotificationScheduler(
+ getApplicationContext(),
+ (android.app.AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)
+ );
+ scheduler.cancelNotification(notificationId);
+
+ Log.i(TAG, "DN|DISMISS_OK id=" + notificationId);
+ return Result.success();
+
+ } catch (Exception e) {
+ Log.e(TAG, "DN|DISMISS_ERR exception id=" + notificationId + " err=" + e.getMessage(), e);
+ return Result.retry();
+ } finally {
+ Trace.endSection();
+ }
+}
+```
+
+### Key Points
+
+1. **Notification ID**: Use `notificationId.hashCode()` to match the ID used when displaying (line 440: `int notificationId = content.getId().hashCode()`)
+2. **Order**: Cancel the notification FIRST, before removing from storage, so it disappears immediately
+3. **Null check**: Check that NotificationManager is not null before calling cancel()
+4. **Logging**: Add instrumentation log to track cancellation
+
+### Expected Behavior After Fix
+
+1. User clicks "Dismiss" button → Notification disappears immediately from system tray
+2. User clicks notification body → App launches (unchanged behavior)
+3. User swipes notification away → Notification dismissed (Android handles this automatically with `setAutoCancel(true)`)
+
+## Testing Checklist
+
+- [ ] Click dismiss button → Notification disappears immediately
+- [ ] Click notification body → App launches
+- [ ] Swipe notification away → Notification dismissed
+- [ ] Check logs for `DN|DISMISS_CANCEL_NOTIF` entry
+- [ ] Verify notification is removed from storage after dismiss
+- [ ] Verify alarms are cancelled after dismiss
+
+## Related Code
+
+- Notification display: `DailyNotificationWorker.displayNotification()` line 440
+- Notification ID generation: `content.getId().hashCode()`
+- Auto-cancel: `builder.setAutoCancel(true)` line 363 (handles swipe-to-dismiss)
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 97a8a315..68d65bb5 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -1693,10 +1693,21 @@ export class CapacitorPlatformService
// Step 3: Get API server from config or use default
// This ensures the plugin knows where to fetch content from
- const apiServer =
+ let apiServer =
config.apiServer ||
(await import("../../constants/app")).DEFAULT_ENDORSER_API_SERVER;
+ // Step 3.5: Convert localhost to 10.0.2.2 for Android emulators
+ // Android emulators can't reach localhost - they need 10.0.2.2 to access the host machine
+ const platform = Capacitor.getPlatform();
+ if (platform === "android" && apiServer) {
+ // Replace localhost or 127.0.0.1 with 10.0.2.2 for Android emulator compatibility
+ apiServer = apiServer.replace(
+ /http:\/\/(localhost|127\.0\.0\.1)(:\d+)?/,
+ "http://10.0.2.2$2",
+ );
+ }
+
// Step 4: Configure plugin with credentials
// Plugin stores these in its Room database for background workers
await DailyNotification.configureNativeFetcher({
diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue
index ab107e62..1f8b7f89 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -825,6 +825,7 @@ import {
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
+import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
@@ -1547,6 +1548,33 @@ export default class AccountViewView extends Vue {
settingsSaved: true,
timestamp: new Date().toISOString(),
});
+
+ // Refresh native fetcher configuration with new API server
+ // This ensures background notification prefetch uses the updated endpoint
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+ const settings = await this.$accountSettings();
+ const starredPlanHandleIds = settings.starredPlanHandleIds || [];
+
+ await platformService.configureNativeFetcher({
+ apiServer: newApiServer,
+ jwt: "", // Will be generated automatically by configureNativeFetcher
+ starredPlanHandleIds,
+ });
+
+ logger.info(
+ "[AccountViewView] Native fetcher configuration refreshed after API server change",
+ {
+ newApiServer,
+ },
+ );
+ } catch (error) {
+ logger.error(
+ "[AccountViewView] Failed to refresh native fetcher config after API server change:",
+ error,
+ );
+ // Don't throw - API server change should still succeed even if native fetcher refresh fails
+ }
}
async onClickSavePartnerServer(): Promise {
--
2.30.2
From 2deb84aa4254c1108abfd1ac2431247942dde5cc Mon Sep 17 00:00:00 2001
From: Matthew Raymer
Date: Tue, 11 Nov 2025 08:14:04 +0000
Subject: [PATCH 17/17] fix: Store starred plans in SharedPreferences when
configuring native fetcher
- Add updateStarredPlans call in configureNativeFetcher to store starred
plan IDs in SharedPreferences for the native fetcher to use
- Clear starred plans if none provided to ensure state consistency
- Add starredPlansCount to configuration log for debugging
This fixes the issue where the native fetcher was querying the API
with an empty planIds array because starred plans weren't being stored
in SharedPreferences. Now when configureNativeFetcher is called with
starredPlanHandleIds, they are properly stored and available for
background prefetch operations.
Also updates package-lock.json for daily-notification-plugin v1.0.11.
---
package-lock.json | 2 +-
.../platforms/CapacitorPlatformService.ts | 23 +++++++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/package-lock.json b/package-lock.json
index be618479..912ecdc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -152,7 +152,7 @@
},
"../daily-notification-plugin": {
"name": "@timesafari/daily-notification-plugin",
- "version": "1.0.8",
+ "version": "1.0.11",
"license": "MIT",
"workspaces": [
"packages/*"
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 68d65bb5..ee50b0e6 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -1716,11 +1716,34 @@ export class CapacitorPlatformService
jwtToken,
});
+ // Step 5: Update starred plans if provided
+ // This stores the starred plan IDs in SharedPreferences for the native fetcher
+ if (
+ config.starredPlanHandleIds &&
+ config.starredPlanHandleIds.length > 0
+ ) {
+ await DailyNotification.updateStarredPlans({
+ planIds: config.starredPlanHandleIds,
+ });
+ logger.info(
+ `[CapacitorPlatformService] Updated starred plans: ${config.starredPlanHandleIds.length} plans`,
+ );
+ } else {
+ // Clear starred plans if none provided
+ await DailyNotification.updateStarredPlans({
+ planIds: [],
+ });
+ logger.info(
+ "[CapacitorPlatformService] Cleared starred plans (none provided)",
+ );
+ }
+
logger.info("[CapacitorPlatformService] Configured native fetcher", {
activeDid,
apiServer,
tokenExpirationHours: 72,
tokenExpirationMinutes: expirationMinutes,
+ starredPlansCount: config.starredPlanHandleIds?.length || 0,
});
} catch (error) {
logger.error(
--
2.30.2